Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
d6be20b9
Commit
d6be20b9
authored
Mar 02, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement masonry view
parent
a8713ec6
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
153 additions
and
99 deletions
+153
-99
MasonryView.tsx
web/src/components/MasonryView/MasonryView.tsx
+66
-0
index.ts
web/src/components/MasonryView/index.ts
+3
-0
MemoDisplaySettingMenu.tsx
web/src/components/MemoDisplaySettingMenu.tsx
+9
-4
PagedMemoList.tsx
web/src/components/PagedMemoList/PagedMemoList.tsx
+21
-12
SearchBar.tsx
web/src/components/SearchBar.tsx
+1
-1
HomeLayout.tsx
web/src/layouts/HomeLayout.tsx
+1
-1
Archived.tsx
web/src/pages/Archived.tsx
+16
-39
Explore.tsx
web/src/pages/Explore.tsx
+14
-18
Home.tsx
web/src/pages/Home.tsx
+17
-23
UserProfile.tsx
web/src/pages/UserProfile.tsx
+1
-1
memoFilter.ts
web/src/store/v1/memoFilter.ts
+4
-0
No files found.
web/src/components/MasonryView/MasonryView.tsx
0 → 100644
View file @
d6be20b9
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
cn
}
from
"@/utils"
;
interface
Props
{
memoList
:
Memo
[];
renderer
:
(
memo
:
Memo
)
=>
JSX
.
Element
;
prefixElement
?:
JSX
.
Element
;
listMode
?:
boolean
;
}
interface
LocalState
{
columns
:
number
;
}
const
MINIMUM_MEMO_VIEWPORT_WIDTH
=
512
;
const
MasonryView
=
(
props
:
Props
)
=>
{
const
[
state
,
setState
]
=
useState
<
LocalState
>
({
columns
:
1
,
});
const
containerRef
=
useRef
<
HTMLDivElement
>
(
null
);
useEffect
(()
=>
{
const
handleResize
=
()
=>
{
if
(
!
containerRef
.
current
)
{
return
;
}
if
(
props
.
listMode
)
{
setState
({
columns
:
1
,
});
return
;
}
const
containerWidth
=
containerRef
.
current
.
offsetWidth
;
const
scale
=
containerWidth
/
MINIMUM_MEMO_VIEWPORT_WIDTH
;
setState
({
columns
:
scale
>
2
?
Math
.
floor
(
scale
)
:
1
,
});
};
handleResize
();
window
.
addEventListener
(
"resize"
,
handleResize
);
return
()
=>
window
.
removeEventListener
(
"resize"
,
handleResize
);
},
[
props
.
listMode
]);
return
(
<
div
ref=
{
containerRef
}
className=
{
cn
(
"w-full grid gap-2"
)
}
style=
{
{
gridTemplateColumns
:
`repeat(${state.columns}, 1fr)`
,
}
}
>
{
Array
.
from
({
length
:
state
.
columns
}).
map
((
_
,
columnIndex
)
=>
(
<
div
key=
{
columnIndex
}
className=
"min-w-0 mx-auto w-full max-w-2xl"
>
{
props
.
prefixElement
&&
columnIndex
===
0
&&
<
div
className=
"mb-2"
>
{
props
.
prefixElement
}
</
div
>
}
{
props
.
memoList
.
filter
((
_
,
index
)
=>
index
%
state
.
columns
===
columnIndex
).
map
((
memo
)
=>
props
.
renderer
(
memo
))
}
</
div
>
))
}
</
div
>
);
};
export
default
MasonryView
;
web/src/components/MasonryView/index.ts
0 → 100644
View file @
d6be20b9
import
MasonryView
from
"./MasonryView"
;
export
default
MasonryView
;
web/src/components/MemoDisplaySettingMenu.tsx
View file @
d6be20b9
import
{
Option
,
Select
}
from
"@mui/joy"
;
import
{
Option
,
Select
,
Switch
}
from
"@mui/joy"
;
import
{
Settings2Icon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useMemoFilterStore
}
from
"@/store/v1"
;
import
{
cn
}
from
"@/utils"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
...
...
@@ -9,10 +10,10 @@ interface Props {
className
?:
string
;
}
const
MemoDisplaySettingMenu
=
({
className
}:
Props
)
=>
{
const
MemoDisplaySettingMenu
=
observer
(
({
className
}:
Props
)
=>
{
const
t
=
useTranslate
();
const
memoFilterStore
=
useMemoFilterStore
();
const
isApplying
=
Boolean
(
memoFilterStore
.
orderByTimeAsc
)
!==
false
;
const
isApplying
=
Boolean
(
memoFilterStore
.
orderByTimeAsc
)
!==
false
||
memoFilterStore
.
masonry
;
return
(
<
Popover
>
...
...
@@ -36,10 +37,14 @@ const MemoDisplaySettingMenu = ({ className }: Props) => {
<
Option
value=
{
true
}
>
{
t
(
"memo.direction-asc"
)
}
</
Option
>
</
Select
>
</
div
>
<
div
className=
"w-full flex flex-row justify-between items-center"
>
<
span
className=
"text-sm shrink-0 mr-3"
>
Masonry View
</
span
>
<
Switch
checked=
{
memoFilterStore
.
masonry
}
onChange=
{
(
event
)
=>
memoFilterStore
.
setMasonry
(
event
.
target
.
checked
)
}
/>
</
div
>
</
div
>
</
PopoverContent
>
</
Popover
>
);
};
}
)
;
export
default
MemoDisplaySettingMenu
;
web/src/components/PagedMemoList/PagedMemoList.tsx
View file @
d6be20b9
import
{
Button
}
from
"@usememos/mui"
;
import
{
ArrowDownIcon
,
ArrowUpIcon
,
LoaderIcon
,
SlashIcon
}
from
"lucide-react"
;
import
{
ArrowDownIcon
,
ArrowUpIcon
,
LoaderIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
matchPath
}
from
"react-router-dom"
;
import
PullToRefresh
from
"react-simple-pull-to-refresh"
;
import
{
DEFAULT_LIST_MEMOS_PAGE_SIZE
}
from
"@/helpers/consts"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
{
useMemoList
,
useMemoStore
}
from
"@/store/v1"
;
import
{
Routes
}
from
"@/router"
;
import
{
useMemoFilterStore
,
useMemoList
,
useMemoStore
}
from
"@/store/v1"
;
import
{
Direction
,
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
import
Empty
from
"../Empty"
;
import
MasonryView
from
"../MasonryView"
;
import
MemoEditor
from
"../MemoEditor"
;
interface
Props
{
renderer
:
(
memo
:
Memo
)
=>
JSX
.
Element
;
...
...
@@ -26,16 +31,18 @@ interface LocalState {
nextPageToken
:
string
;
}
const
PagedMemoList
=
(
props
:
Props
)
=>
{
const
PagedMemoList
=
observer
(
(
props
:
Props
)
=>
{
const
t
=
useTranslate
();
const
{
md
}
=
useResponsiveWidth
();
const
memoStore
=
useMemoStore
();
const
memoList
=
useMemoList
();
const
memoFilterStore
=
useMemoFilterStore
();
const
[
state
,
setState
]
=
useState
<
LocalState
>
({
isRequesting
:
true
,
// Initial request
nextPageToken
:
""
,
});
const
sortedMemoList
=
props
.
listSort
?
props
.
listSort
(
memoList
.
value
)
:
memoList
.
value
;
const
showMemoEditor
=
Boolean
(
matchPath
(
Routes
.
ROOT
,
window
.
location
.
pathname
));
const
fetchMoreMemos
=
async
(
nextPageToken
:
string
)
=>
{
setState
((
state
)
=>
({
...
state
,
isRequesting
:
true
}));
...
...
@@ -66,7 +73,12 @@ const PagedMemoList = (props: Props) => {
const
children
=
(
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
{
sortedMemoList
.
map
((
memo
)
=>
props
.
renderer
(
memo
))
}
<
MasonryView
memoList=
{
sortedMemoList
}
renderer=
{
props
.
renderer
}
prefixElement=
{
showMemoEditor
?
<
MemoEditor
className=
"mb-2"
cacheKey=
"home-memo-editor"
/>
:
undefined
}
listMode=
{
!
memoFilterStore
.
masonry
}
/>
{
state
.
isRequesting
&&
(
<
div
className=
"w-full flex flex-row justify-center items-center my-4"
>
<
LoaderIcon
className=
"animate-spin text-zinc-500"
/>
...
...
@@ -82,13 +94,10 @@ const PagedMemoList = (props: Props) => {
)
:
(
<
div
className=
"w-full flex flex-row justify-center items-center my-4"
>
{
state
.
nextPageToken
&&
(
<>
<
Button
variant=
"plain"
onClick=
{
()
=>
fetchMoreMemos
(
state
.
nextPageToken
)
}
>
{
t
(
"memo.load-more"
)
}
<
ArrowDownIcon
className=
"ml-1 w-4 h-auto"
/>
</
Button
>
<
SlashIcon
className=
"mx-1 w-4 h-auto opacity-40"
/>
</>
<
Button
variant=
"plain"
onClick=
{
()
=>
fetchMoreMemos
(
state
.
nextPageToken
)
}
>
{
t
(
"memo.load-more"
)
}
<
ArrowDownIcon
className=
"ml-1 w-4 h-auto"
/>
</
Button
>
)
}
<
BackToTop
/>
</
div
>
...
...
@@ -120,7 +129,7 @@ const PagedMemoList = (props: Props) => {
{
children
}
</
PullToRefresh
>
);
};
}
)
;
const
BackToTop
=
()
=>
{
const
t
=
useTranslate
();
...
...
web/src/components/SearchBar.tsx
View file @
d6be20b9
...
...
@@ -39,7 +39,7 @@ const SearchBar = () => {
onChange=
{
onTextChange
}
onKeyDown=
{
onKeyDown
}
/>
<
MemoDisplaySettingMenu
className=
"absolute right-2 top-2
.5
"
/>
<
MemoDisplaySettingMenu
className=
"absolute right-2 top-2"
/>
</
div
>
);
};
...
...
web/src/layouts/HomeLayout.tsx
View file @
d6be20b9
...
...
@@ -27,7 +27,7 @@ const HomeLayout = observer(() => {
</
div
>
)
}
<
div
className=
{
cn
(
"w-full min-h-full"
,
lg
?
"pl-72"
:
md
?
"pl-56"
:
""
)
}
>
<
div
className=
{
cn
(
"w-full mx-auto px-4 sm:px-6 md:pt-6 pb-8"
,
md
&&
"max-w-3xl"
)
}
>
<
div
className=
{
cn
(
"w-full mx-auto px-4 sm:px-6 md:pt-6 pb-8"
)
}
>
<
Outlet
/>
</
div
>
</
div
>
...
...
web/src/pages/Archived.tsx
View file @
d6be20b9
import
dayjs
from
"dayjs"
;
import
{
ArchiveIcon
}
from
"lucide-react"
;
import
{
useMemo
}
from
"react"
;
import
MemoFilters
from
"@/components/MemoFilters"
;
import
MemoView
from
"@/components/MemoView"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
PagedMemoList
from
"@/components/PagedMemoList"
;
import
SearchBar
from
"@/components/SearchBar"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
import
{
useMemoFilterStore
}
from
"@/store/v1"
;
import
{
Direction
,
State
}
from
"@/types/proto/api/v1/common"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
import
{
useTranslate
}
from
"@/utils/i18n"
;
const
Archived
=
()
=>
{
const
t
=
useTranslate
();
const
user
=
useCurrentUser
();
const
memoFilterStore
=
useMemoFilterStore
();
...
...
@@ -38,39 +32,22 @@ const Archived = () => {
},
[
user
,
memoFilterStore
.
filters
]);
return
(
<
section
className=
"@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8"
>
<
MobileHeader
/>
<
div
className=
"w-full px-4 sm:px-6"
>
<
div
className=
"w-full flex flex-col justify-start items-start"
>
<
div
className=
"w-full flex flex-row justify-between items-center mb-2"
>
<
div
className=
"flex flex-row justify-start items-center gap-1"
>
<
ArchiveIcon
className=
"w-5 h-auto opacity-70 shrink-0"
/>
<
span
>
{
t
(
"common.archived"
)
}
</
span
>
</
div
>
<
div
className=
"w-44"
>
<
SearchBar
/>
</
div
>
</
div
>
<
MemoFilters
/>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.updateTime}`
}
memo=
{
memo
}
showVisibility
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
ARCHIVED
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
}
owner=
{
user
.
name
}
state=
{
State
.
ARCHIVED
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
oldFilter=
{
memoListFilter
}
/>
</
div
>
</
div
>
</
section
>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.updateTime}`
}
memo=
{
memo
}
showVisibility
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
ARCHIVED
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
}
owner=
{
user
.
name
}
state=
{
State
.
ARCHIVED
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
oldFilter=
{
memoListFilter
}
/>
);
};
...
...
web/src/pages/Explore.tsx
View file @
d6be20b9
...
...
@@ -44,24 +44,20 @@ const Explore = () => {
},
[
user
,
memoFilterStore
.
filters
,
memoFilterStore
.
orderByTimeAsc
]);
return
(
<>
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.updateTime}`
}
memo=
{
memo
}
showCreator
showVisibility
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
NORMAL
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
oldFilter=
{
memoListFilter
}
/>
</
div
>
</>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.updateTime}`
}
memo=
{
memo
}
showCreator
showVisibility
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
NORMAL
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
oldFilter=
{
memoListFilter
}
/>
);
};
...
...
web/src/pages/Home.tsx
View file @
d6be20b9
import
dayjs
from
"dayjs"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useMemo
}
from
"react"
;
import
MemoEditor
from
"@/components/MemoEditor"
;
import
MemoView
from
"@/components/MemoView"
;
import
PagedMemoList
from
"@/components/PagedMemoList"
;
import
useCurrentUser
from
"@/hooks/useCurrentUser"
;
...
...
@@ -48,28 +47,23 @@ const Home = observer(() => {
},
[
user
,
memoFilterStore
.
filters
,
memoFilterStore
.
orderByTimeAsc
]);
return
(
<>
<
MemoEditor
className=
"mb-2"
cacheKey=
"home-memo-editor"
/>
<
div
className=
"flex flex-col justify-start items-start w-full max-w-full"
>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.displayTime}`
}
memo=
{
memo
}
showVisibility
showPinned
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
NORMAL
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
.
sort
((
a
,
b
)
=>
Number
(
b
.
pinned
)
-
Number
(
a
.
pinned
))
}
owner=
{
user
.
name
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
filter=
{
selectedShortcut
?.
filter
||
""
}
oldFilter=
{
memoListFilter
}
/>
</
div
>
</>
<
PagedMemoList
renderer=
{
(
memo
:
Memo
)
=>
<
MemoView
key=
{
`${memo.name}-${memo.displayTime}`
}
memo=
{
memo
}
showVisibility
showPinned
compact
/>
}
listSort=
{
(
memos
:
Memo
[])
=>
memos
.
filter
((
memo
)
=>
memo
.
state
===
State
.
NORMAL
)
.
sort
((
a
,
b
)
=>
memoFilterStore
.
orderByTimeAsc
?
dayjs
(
a
.
displayTime
).
unix
()
-
dayjs
(
b
.
displayTime
).
unix
()
:
dayjs
(
b
.
displayTime
).
unix
()
-
dayjs
(
a
.
displayTime
).
unix
(),
)
.
sort
((
a
,
b
)
=>
Number
(
b
.
pinned
)
-
Number
(
a
.
pinned
))
}
owner=
{
user
.
name
}
direction=
{
memoFilterStore
.
orderByTimeAsc
?
Direction
.
ASC
:
Direction
.
DESC
}
filter=
{
selectedShortcut
?.
filter
||
""
}
oldFilter=
{
memoListFilter
}
/>
);
});
...
...
web/src/pages/UserProfile.tsx
View file @
d6be20b9
...
...
@@ -76,7 +76,7 @@ const UserProfile = () => {
};
return
(
<
section
className=
"w-full max-w-
5xl
min-h-full flex flex-col justify-start items-center pb-8"
>
<
section
className=
"w-full max-w-
3xl mx-auto
min-h-full flex flex-col justify-start items-center pb-8"
>
<
div
className=
"w-full px-4 sm:px-6 flex flex-col justify-start items-center"
>
{
!
loadingState
.
isLoading
&&
(
user
?
(
...
...
web/src/store/v1/memoFilter.ts
View file @
d6be20b9
...
...
@@ -43,6 +43,8 @@ interface State {
orderByTimeAsc
:
boolean
;
// The id of selected shortcut.
shortcut
?:
string
;
// TODO: Remove this when the masonry view is implemented.
masonry
:
boolean
;
}
const
getInitialState
=
():
State
=>
{
...
...
@@ -50,6 +52,7 @@ const getInitialState = (): State => {
return
{
filters
:
parseFilterQuery
(
searchParams
.
get
(
"filter"
)),
orderByTimeAsc
:
searchParams
.
get
(
"orderBy"
)
===
"asc"
,
masonry
:
false
,
};
};
...
...
@@ -62,5 +65,6 @@ export const useMemoFilterStore = create(
removeFilter
:
(
filterFn
:
(
f
:
MemoFilter
)
=>
boolean
)
=>
set
((
state
)
=>
({
filters
:
state
.
filters
.
filter
((
f
)
=>
!
filterFn
(
f
))
})),
setOrderByTimeAsc
:
(
orderByTimeAsc
:
boolean
)
=>
set
({
orderByTimeAsc
}),
setShortcut
:
(
shortcut
?:
string
)
=>
set
({
shortcut
}),
setMasonry
:
(
masonry
:
boolean
)
=>
set
({
masonry
}),
})),
);
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment