diff --git a/common/image.go b/common/image.go deleted file mode 100644 index cd71160db..000000000 --- a/common/image.go +++ /dev/null @@ -1,76 +0,0 @@ -package common - -import ( - "bytes" - "fmt" - "image" - "image/jpeg" - "image/png" - "os" - "path/filepath" - "strings" - - "github.com/disintegration/imaging" -) - -const ( - ThumbnailDir = ".thumbnail_cache" - ThumbnailSize = 302 // Thumbnail size should be defined by frontend -) - -func ResizeImageFile(dst, src string, mime string) error { - srcBytes, err := os.ReadFile(src) - if err != nil { - return fmt.Errorf("Failed to open %s: %s", src, err) - } - - dstBytes, err := ResizeImageBlob(srcBytes, ThumbnailSize, mime) - if err != nil { - return fmt.Errorf("Failed to resise %s: %s", src, err) - } - - err = os.MkdirAll(filepath.Dir(dst), os.ModePerm) - if err != nil { - return fmt.Errorf("Failed to mkdir for %s: %s", dst, err) - } - - err = os.WriteFile(dst, dstBytes, 0666) - if err != nil { - return fmt.Errorf("Failed to write %s: %s", dst, err) - } - - return nil -} - -func ResizeImageBlob(data []byte, maxSize int, mime string) ([]byte, error) { - var err error - var oldImage image.Image - - switch strings.ToLower(mime) { - case "image/jpeg": - oldImage, err = jpeg.Decode(bytes.NewReader(data)) - case "image/png": - oldImage, err = png.Decode(bytes.NewReader(data)) - default: - return nil, fmt.Errorf("mime %s is not support", mime) - } - - if err != nil { - return nil, err - } - - newImage := imaging.Resize(oldImage, maxSize, 0, imaging.NearestNeighbor) - - var newBuffer bytes.Buffer - switch mime { - case "image/jpeg": - err = jpeg.Encode(&newBuffer, newImage, nil) - case "image/png": - err = png.Encode(&newBuffer, newImage) - } - if err != nil { - return nil, err - } - - return newBuffer.Bytes(), nil -} diff --git a/go.mod b/go.mod index 0783fb254..eee62b812 100644 --- a/go.mod +++ b/go.mod @@ -20,12 +20,12 @@ require ( go.uber.org/zap v1.24.0 golang.org/x/crypto v0.1.0 golang.org/x/exp v0.0.0-20230111222715-75897c7a292a - golang.org/x/mod v0.6.0 + golang.org/x/mod v0.8.0 golang.org/x/net v0.7.0 golang.org/x/oauth2 v0.5.0 ) -require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect +require golang.org/x/image v0.7.0 // indirect require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect @@ -69,7 +69,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.1.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 76e9ee82d..20796ee6d 100644 --- a/go.sum +++ b/go.sum @@ -261,6 +261,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -281,6 +282,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= @@ -298,8 +300,9 @@ golang.org/x/exp v0.0.0-20230111222715-75897c7a292a h1:/YWeLOBWYV5WAQORVPkZF3Pq9 golang.org/x/exp v0.0.0-20230111222715-75897c7a292a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -321,8 +324,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -354,6 +358,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -377,6 +383,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -414,18 +422,24 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -478,6 +492,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server/resource.go b/server/resource.go index 731d15d40..2c3af2ac3 100644 --- a/server/resource.go +++ b/server/resource.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/disintegration/imaging" "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/usememos/memos/api" @@ -30,6 +31,9 @@ const ( // This is unrelated to maximum upload size limit, which is now set through system setting. maxUploadBufferSizeBytes = 32 << 20 MebiByte = 1024 * 1024 + + // thumbnailImagePath is the directory to store image thumbnails. + thumbnailImagePath = ".thumbnail_cache" ) var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`) @@ -163,14 +167,6 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to copy file").SetInternal(err) } - if filetype == "image/jpeg" || filetype == "image/png" { - thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, publicID) - err := common.ResizeImageFile(thumbnailPath, filePath, filetype) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate thumbnail").SetInternal(err) - } - } - resourceCreate = &api.ResourceCreate{ CreatorID: userID, Filename: filename, @@ -323,14 +319,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) { } if resource.InternalPath != "" { - err := os.Remove(resource.InternalPath) - if err != nil { + if err := os.Remove(resource.InternalPath); err != nil { log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err)) } - thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID) - err = os.Remove(thumbnailPath) - if err != nil { + thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID) + if err := os.Remove(thumbnailPath); err != nil { log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err)) } } @@ -423,22 +417,6 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) { blob := resource.Blob if resource.InternalPath != "" { resourcePath := resource.InternalPath - if c.QueryParam("thumbnail") == "1" && (resource.Type == "image/jpeg" || resource.Type == "image/png") { - thumbnailPath := path.Join(s.Profile.Data, common.ThumbnailDir, resource.PublicID) - if _, err := os.Stat(thumbnailPath); err == nil { - resourcePath = thumbnailPath - } else if os.IsNotExist(err) { - err := common.ResizeImageFile(thumbnailPath, resourcePath, resource.Type) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to resize resource: %s", resourcePath)).SetInternal(err) - } - - resourcePath = thumbnailPath - } else { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to check resource thumbnail stat: %s", thumbnailPath)).SetInternal(err) - } - } - src, err := os.Open(resourcePath) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", resourcePath)).SetInternal(err) @@ -450,6 +428,36 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) { } } + if c.QueryParam("thumbnail") == "1" && common.HasPrefixes(resource.Type, "image/png", "image/jpeg") { + ext := filepath.Ext(filename) + thumbnailPath := path.Join(s.Profile.Data, thumbnailImagePath, resource.PublicID+ext) + if _, err := os.Stat(thumbnailPath); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to check thumbnail image stat: %s", thumbnailPath)).SetInternal(err) + } + + reader := bytes.NewReader(blob) + src, err := imaging.Decode(reader) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to decode thumbnail image: %s", thumbnailPath)).SetInternal(err) + } + thumbnailImage := imaging.Resize(src, 512, 0, imaging.Lanczos) + if err := imaging.Save(thumbnailImage, thumbnailPath); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to resize thumbnail image: %s", thumbnailPath)).SetInternal(err) + } + } + + src, err := os.Open(thumbnailPath) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", thumbnailPath)).SetInternal(err) + } + defer src.Close() + blob, err = io.ReadAll(src) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to read the local resource: %s", thumbnailPath)).SetInternal(err) + } + } + c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable") c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'") resourceType := strings.ToLower(resource.Type) diff --git a/web/src/components/ArchivedMemo.tsx b/web/src/components/ArchivedMemo.tsx index 5cd12ee8e..765078e55 100644 --- a/web/src/components/ArchivedMemo.tsx +++ b/web/src/components/ArchivedMemo.tsx @@ -4,7 +4,7 @@ import { useMemoStore } from "@/store/module"; import { getDateTimeString } from "@/helpers/datetime"; import useToggle from "@/hooks/useToggle"; import MemoContent from "./MemoContent"; -import MemoResources from "./MemoResources"; +import MemoResourceListView from "./MemoResourceListView"; import "@/less/memo.less"; interface Props { @@ -67,7 +67,7 @@ const ArchivedMemo: React.FC = (props: Props) => { - + ); }; diff --git a/web/src/components/CreateResourceDialog.tsx b/web/src/components/CreateResourceDialog.tsx index 7c9806323..aa25cf473 100644 --- a/web/src/components/CreateResourceDialog.tsx +++ b/web/src/components/CreateResourceDialog.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete, Tooltip } from "@mui/joy"; +import { Button, Input, Select, Option, Typography, List, ListItem, Autocomplete } from "@mui/joy"; import React, { useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; @@ -215,31 +215,29 @@ const CreateResourceDialog: React.FC = (props: Props) => { {fileList.map((file, index) => ( - - - {file.name} -
- - -
-
-
+ + {file.name} +
+ + +
+
))}
diff --git a/web/src/components/DailyMemo.tsx b/web/src/components/DailyMemo.tsx index c6ec38091..450d940d6 100644 --- a/web/src/components/DailyMemo.tsx +++ b/web/src/components/DailyMemo.tsx @@ -1,6 +1,6 @@ import { getTimeString } from "@/helpers/datetime"; import MemoContent from "./MemoContent"; -import MemoResources from "./MemoResources"; +import MemoResourceListView from "./MemoResourceListView"; import "@/less/daily-memo.less"; interface Props { @@ -18,7 +18,7 @@ const DailyMemo: React.FC = (props: Props) => {
- +
diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index d65ec82a2..a6d7f7f6a 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -12,11 +12,11 @@ import Divider from "./kit/Divider"; import { showCommonDialog } from "./Dialog/CommonDialog"; import Icon from "./Icon"; import MemoContent from "./MemoContent"; -import MemoResources from "./MemoResources"; +import MemoResourceListView from "./MemoResourceListView"; +import MemoRelationListView from "./MemoRelationListView"; import showShareMemo from "./ShareMemoDialog"; import showPreviewImageDialog from "./PreviewImageDialog"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; -import MemoRelationListView from "./MemoRelationListView"; import "@/less/memo.less"; interface Props { @@ -39,9 +39,17 @@ const Memo: React.FC = (props: Props) => { const isVisitorMode = userStore.isVisitorMode() || readonly; useEffect(() => { - Promise.all(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then((memoList) => { - setRelatedMemoList(uniqWith(memoList, isEqual)); - }); + 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]); useEffect(() => { @@ -271,13 +279,13 @@ const Memo: React.FC = (props: Props) => { onMemoContentClick={handleMemoContentClick} onMemoContentDoubleClick={handleMemoContentDoubleClick} /> - + {!showRelatedMemos && } {showRelatedMemos && relatedMemoList.length > 0 && ( <> -

+

Related memos

diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index 7944d7ef7..f14f40e42 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -88,6 +88,10 @@ const MemoEditor = () => { prevEditorStateRef.current = editorState; }, [editorState.editMemoId]); + useEffect(() => { + handleEditorFocus(); + }, [editorStore.state.relationList]); + const handleKeyDown = (event: React.KeyboardEvent) => { if (!editorRef.current) { return; diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx index ff0fbecf5..74b941ebd 100644 --- a/web/src/components/MemoRelationListView.tsx +++ b/web/src/components/MemoRelationListView.tsx @@ -20,6 +20,10 @@ const MemoRelationListView = (props: Props) => { fetchRelatedMemoList(); }, [relationList]); + const handleGotoMemoDetail = (memo: Memo) => { + window.open(`/m/${memo.id}`, "_blank"); + }; + return ( <> {relatedMemoList.length > 0 && ( @@ -29,6 +33,7 @@ const MemoRelationListView = (props: Props) => {
handleGotoMemoDetail(memo)} >
diff --git a/web/src/components/MemoResources.tsx b/web/src/components/MemoResourceListView.tsx similarity index 96% rename from web/src/components/MemoResources.tsx rename to web/src/components/MemoResourceListView.tsx index c6ffbe810..f2792ec45 100644 --- a/web/src/components/MemoResources.tsx +++ b/web/src/components/MemoResourceListView.tsx @@ -17,7 +17,7 @@ const getDefaultProps = (): Props => { }; }; -const MemoResources: React.FC = (props: Props) => { +const MemoResourceListView: React.FC = (props: Props) => { const { className, resourceList } = { ...getDefaultProps(), ...props, @@ -75,4 +75,4 @@ const MemoResources: React.FC = (props: Props) => { ); }; -export default MemoResources; +export default MemoResourceListView; diff --git a/web/src/components/ResourceCover.tsx b/web/src/components/ResourceCover.tsx index 42e6ac451..d102d3fe6 100644 --- a/web/src/components/ResourceCover.tsx +++ b/web/src/components/ResourceCover.tsx @@ -2,6 +2,7 @@ import React from "react"; import Icon from "./Icon"; import { getResourceUrl } from "@/utils/resource"; import showPreviewImageDialog from "./PreviewImageDialog"; +import SquareDiv from "./kit/SquareDiv"; import "@/less/resource-cover.less"; interface ResourceCoverProps { @@ -40,7 +41,13 @@ const ResourceCover = ({ resource }: ResourceCoverProps) => { switch (resourceType) { case "image/*": return ( - showPreviewImageDialog(resourceUrl)} /> + + showPreviewImageDialog(resourceUrl)} + /> + ); case "video/*": return ; diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 80a58d7e4..7bd099d0f 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -14,7 +14,7 @@ import useLoading from "@/hooks/useLoading"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import MemoContent from "./MemoContent"; -import MemoResources from "./MemoResources"; +import MemoResourceListView from "./MemoResourceListView"; import showEmbedMemoDialog from "./EmbedMemoDialog"; import "@/less/share-memo-dialog.less"; @@ -177,7 +177,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { {memo.createdAtStr}
- +
diff --git a/web/src/components/kit/DatePicker.tsx b/web/src/components/kit/DatePicker.tsx index 519c1651e..2eb7786f0 100644 --- a/web/src/components/kit/DatePicker.tsx +++ b/web/src/components/kit/DatePicker.tsx @@ -18,12 +18,12 @@ const DatePicker: React.FC = (props: DatePickerProps) => { const { className, datestamp, handleDateStampChange } = props; const [currentDateStamp, setCurrentDateStamp] = useState(getMonthFirstDayDateStamp(datestamp)); const [countByDate, setCountByDate] = useState(new Map()); + const currentUserId = useUserStore().getCurrentUserId(); useEffect(() => { setCurrentDateStamp(getMonthFirstDayDateStamp(datestamp)); }, [datestamp]); - const currentUserId = useUserStore().getCurrentUserId(); useEffect(() => { getMemoStats(currentUserId).then(({ data: { data } }) => { const m = new Map(); diff --git a/web/src/pages/EmbedMemo.tsx b/web/src/pages/EmbedMemo.tsx index c928ba24f..e544e8fb5 100644 --- a/web/src/pages/EmbedMemo.tsx +++ b/web/src/pages/EmbedMemo.tsx @@ -5,7 +5,7 @@ import { UNKNOWN_ID } from "@/helpers/consts"; import { useMemoStore } from "@/store/module"; import useLoading from "@/hooks/useLoading"; import MemoContent from "@/components/MemoContent"; -import MemoResources from "@/components/MemoResources"; +import MemoResourceListView from "@/components/MemoResourceListView"; import { getDateTimeString } from "@/helpers/datetime"; interface State { @@ -51,7 +51,7 @@ const EmbedMemo = () => {
undefined} /> - +
)} diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 8d0a797eb..19994e1de 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -3,8 +3,9 @@ import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { Link, useLocation, useParams } from "react-router-dom"; import { UNKNOWN_ID } from "@/helpers/consts"; -import { useGlobalStore, useMemoStore, useUserStore } from "@/store/module"; +import { useGlobalStore, useMemoStore } from "@/store/module"; import useLoading from "@/hooks/useLoading"; +import Icon from "@/components/Icon"; import Memo from "@/components/Memo"; interface State { @@ -17,7 +18,6 @@ const MemoDetail = () => { const location = useLocation(); const globalStore = useGlobalStore(); const memoStore = useMemoStore(); - const userStore = useUserStore(); const [state, setState] = useState({ memo: { id: UNKNOWN_ID, @@ -25,7 +25,6 @@ const MemoDetail = () => { }); const loadingState = useLoading(); const customizedProfile = globalStore.state.systemStatus.customizedProfile; - const user = userStore.state.user; useEffect(() => { const memoId = Number(params.memoId); @@ -47,38 +46,27 @@ const MemoDetail = () => { return (
-
-
+
+

{customizedProfile.name}

-
- {!loadingState.isLoading && ( - <> - {user ? ( - - 🏠 {t("router.back-to-home")} - - ) : ( - - 👉 {t("common.sign-in")} - - )} - - )} -
{!loadingState.isLoading && ( -
- -
+ <> +
+ +
+
+ + {t("router.back-to-home")} + +
+ )}