Browse Source

Merge pull request #7798 from weseek/imprv/suppress-unnecessary-bookmark-api-requests-2

imprv: Suppress unnecessary bookmark API requests
Yuki Takei 2 years ago
parent
commit
a1377ca8cf

+ 52 - 29
apps/app/src/components/BookmarkButtons.tsx

@@ -3,38 +3,49 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import {
-  UncontrolledTooltip, Popover, PopoverBody, DropdownToggle,
-} from 'reactstrap';
+import DropdownToggle from 'reactstrap/es/DropdownToggle';
+import Popover from 'reactstrap/es/Popover';
+import PopoverBody from 'reactstrap/es/PopoverBody';
+import UncontrolledTooltip from 'reactstrap/es/UncontrolledTooltip';
 
 
-import { IBookmarkInfo } from '~/interfaces/bookmark-info';
+import { useSWRxBookmarkedUsers } from '~/stores/bookmark';
 import { useIsGuestUser } from '~/stores/context';
 import { useIsGuestUser } from '~/stores/context';
 
 
-import { IUser } from '../interfaces/user';
-
 import { BookmarkFolderMenu } from './Bookmarks/BookmarkFolderMenu';
 import { BookmarkFolderMenu } from './Bookmarks/BookmarkFolderMenu';
 import UserPictureList from './User/UserPictureList';
 import UserPictureList from './User/UserPictureList';
 
 
 import styles from './BookmarkButtons.module.scss';
 import styles from './BookmarkButtons.module.scss';
 
 
 interface Props {
 interface Props {
-  bookmarkedUsers?: IUser[]
-  hideTotalNumber?: boolean
-  bookmarkInfo? : IBookmarkInfo
+  pageId: string,
+  isBookmarked?: boolean,
+  bookmarkCount: number,
+  hideTotalNumber?: boolean,
 }
 }
 
 
 export const BookmarkButtons: FC<Props> = (props: Props) => {
 export const BookmarkButtons: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    bookmarkedUsers, hideTotalNumber, bookmarkInfo,
+    pageId, isBookmarked, bookmarkCount, hideTotalNumber,
   } = props;
   } = props;
 
 
-  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+  const [isBookmarkFolderMenuOpen, setBookmarkFolderMenuOpen] = useState(false);
+  const [isBookmarkUsersPopoverOpen, setBookmarkUsersPopoverOpen] = useState(false);
 
 
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
-  const togglePopover = () => {
-    setIsPopoverOpen(!isPopoverOpen);
+  const { data: bookmarkedUsers, isLoading: isLoadingBookmarkedUsers } = useSWRxBookmarkedUsers(isBookmarkUsersPopoverOpen ? pageId : null);
+
+  const unbookmarkHandler = () => {
+    setBookmarkFolderMenuOpen(false);
+  };
+
+  const toggleBookmarkFolderMenuHandler = () => {
+    setBookmarkFolderMenuOpen(v => !v);
+  };
+
+  const toggleBookmarkUsersPopover = () => {
+    setBookmarkUsersPopoverOpen(v => !v);
   };
   };
 
 
   const getTooltipMessage = useCallback(() => {
   const getTooltipMessage = useCallback(() => {
@@ -45,19 +56,23 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
     return 'tooltip.bookmark';
     return 'tooltip.bookmark';
   }, [isGuestUser]);
   }, [isGuestUser]);
 
 
-  if (bookmarkInfo == null) {
+  if (pageId == null) {
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
     <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">
     <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">
-      <BookmarkFolderMenu bookmarkInfo={bookmarkInfo}>
+
+      <BookmarkFolderMenu
+        isOpen={isBookmarkFolderMenuOpen} pageId={pageId} isBookmarked={isBookmarked ?? false}
+        onToggle={toggleBookmarkFolderMenuHandler}
+        onUnbookmark={unbookmarkHandler}
+      >
         <DropdownToggle id='bookmark-dropdown-btn' color="transparent" className={`shadow-none btn btn-bookmark border-0
         <DropdownToggle id='bookmark-dropdown-btn' color="transparent" className={`shadow-none btn btn-bookmark border-0
-          ${bookmarkInfo.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}>
-          <i className={`fa ${bookmarkInfo.isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
+          ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}>
+          <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
         </DropdownToggle>
         </DropdownToggle>
       </BookmarkFolderMenu>
       </BookmarkFolderMenu>
-
       <UncontrolledTooltip placement="top" data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
       <UncontrolledTooltip placement="top" data-testid="bookmark-button-tooltip" target="bookmark-dropdown-btn" fade={false}>
         {t(getTooltipMessage())}
         {t(getTooltipMessage())}
       </UncontrolledTooltip>
       </UncontrolledTooltip>
@@ -68,19 +83,27 @@ export const BookmarkButtons: FC<Props> = (props: Props) => {
             type="button"
             type="button"
             id="po-total-bookmarks"
             id="po-total-bookmarks"
             className={`shadow-none btn btn-bookmark border-0
             className={`shadow-none btn btn-bookmark border-0
-              total-bookmarks ${bookmarkInfo.isBookmarked ? 'active' : ''}`}
+              total-bookmarks ${isBookmarked ? 'active' : ''}`}
           >
           >
-            {bookmarkInfo.sumOfBookmarks ?? 0}
+            {bookmarkCount}
           </button>
           </button>
-          { bookmarkedUsers != null && (
-            <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
-              <PopoverBody className="user-list-popover">
-                <div className="px-2 text-right user-list-content text-truncate text-muted">
-                  {bookmarkedUsers.length ? <UserPictureList users={props.bookmarkedUsers} /> : t('No users have bookmarked yet')}
-                </div>
-              </PopoverBody>
-            </Popover>
-          ) }
+          <Popover placement="bottom" isOpen={isBookmarkUsersPopoverOpen} target="po-total-bookmarks" toggle={toggleBookmarkUsersPopover} trigger="legacy">
+            <PopoverBody className="user-list-popover">
+              { isLoadingBookmarkedUsers && <i className="fa fa-spinner fa-pulse"></i> }
+              { !isLoadingBookmarkedUsers && bookmarkedUsers != null && (
+                <>
+                  { bookmarkedUsers.length > 0
+                    ? (
+                      <div className="px-2 text-right user-list-content text-truncate text-muted">
+                        <UserPictureList users={bookmarkedUsers} />
+                      </div>
+                    )
+                    : t('No users have bookmarked yet')
+                  }
+                </>
+              ) }
+            </PopoverBody>
+          </Popover>
         </>
         </>
       ) }
       ) }
     </div>
     </div>

+ 34 - 23
apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -6,28 +6,37 @@ import { DropdownItem, DropdownMenu, UncontrolledDropdown } from 'reactstrap';
 
 
 import { addBookmarkToFolder, toggleBookmark } from '~/client/util/bookmark-utils';
 import { addBookmarkToFolder, toggleBookmark } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { IBookmarkInfo } from '~/interfaces/bookmark-info';
-import { useSWRMUTxCurrentUserBookmarks, useSWRxBookmarkInfo } from '~/stores/bookmark';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
-import { useSWRxPageInfo } from '~/stores/page';
+import { useSWRMUTxPageInfo } from '~/stores/page';
 
 
 import { BookmarkFolderMenuItem } from './BookmarkFolderMenuItem';
 import { BookmarkFolderMenuItem } from './BookmarkFolderMenuItem';
 
 
-export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode, bookmarkInfo: IBookmarkInfo }> = ({ children, bookmarkInfo }): JSX.Element => {
+
+type BookmarkFolderMenuProps = {
+  isOpen: boolean,
+  pageId: string,
+  isBookmarked: boolean,
+  onToggle?: () => void,
+  onUnbookmark?: () => void,
+  children?: React.ReactNode,
+}
+
+export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element => {
+  const {
+    isOpen, pageId, isBookmarked, onToggle, onUnbookmark, children,
+  } = props;
+
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
-  const [isOpen, setIsOpen] = useState(false);
 
 
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(currentUser?._id);
   const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(currentUser?._id);
-  const { mutate: mutateBookmarkInfo } = useSWRxBookmarkInfo(bookmarkInfo.pageId);
 
 
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
-  const { mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkInfo.pageId);
-
-  const isBookmarked = bookmarkInfo.isBookmarked ?? false;
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
 
 
   const isBookmarkFolderExists = useMemo((): boolean => {
   const isBookmarkFolderExists = useMemo((): boolean => {
     return bookmarkFolders != null && bookmarkFolders.length > 0;
     return bookmarkFolders != null && bookmarkFolders.length > 0;
@@ -35,36 +44,40 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode, bookmarkI
 
 
   const toggleBookmarkHandler = useCallback(async() => {
   const toggleBookmarkHandler = useCallback(async() => {
     try {
     try {
-      await toggleBookmark(bookmarkInfo.pageId, isBookmarked);
+      await toggleBookmark(pageId, isBookmarked);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkInfo.pageId, isBookmarked]);
+  }, [isBookmarked, pageId]);
 
 
   const onUnbookmarkHandler = useCallback(async() => {
   const onUnbookmarkHandler = useCallback(async() => {
+    if (onUnbookmark != null) {
+      onUnbookmark();
+    }
     await toggleBookmarkHandler();
     await toggleBookmarkHandler();
-    setIsOpen(false);
     setSelectedItem(null);
     setSelectedItem(null);
     mutateCurrentUserBookmarks();
     mutateCurrentUserBookmarks();
-    mutateBookmarkInfo();
     mutateBookmarkFolders();
     mutateBookmarkFolders();
     mutatePageInfo();
     mutatePageInfo();
-  }, [mutateBookmarkFolders, mutateBookmarkInfo, mutatePageInfo, mutateCurrentUserBookmarks, toggleBookmarkHandler]);
+  }, [onUnbookmark, toggleBookmarkHandler, mutateCurrentUserBookmarks, mutateBookmarkFolders, mutatePageInfo]);
 
 
   const toggleHandler = useCallback(async() => {
   const toggleHandler = useCallback(async() => {
-    setIsOpen(!isOpen);
-
+    // on close
     if (isOpen && bookmarkFolders != null) {
     if (isOpen && bookmarkFolders != null) {
       bookmarkFolders.forEach((bookmarkFolder) => {
       bookmarkFolders.forEach((bookmarkFolder) => {
         bookmarkFolder.bookmarks.forEach((bookmark) => {
         bookmarkFolder.bookmarks.forEach((bookmark) => {
-          if (bookmark.page._id === bookmarkInfo.pageId) {
+          if (bookmark.page._id === pageId) {
             setSelectedItem(bookmarkFolder._id);
             setSelectedItem(bookmarkFolder._id);
           }
           }
         });
         });
       });
       });
     }
     }
 
 
+    if (onToggle != null) {
+      onToggle();
+    }
+
     if (selectedItem == null) {
     if (selectedItem == null) {
       setSelectedItem('root');
       setSelectedItem('root');
     }
     }
@@ -73,7 +86,6 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode, bookmarkI
       try {
       try {
         await toggleBookmarkHandler();
         await toggleBookmarkHandler();
         mutateCurrentUserBookmarks();
         mutateCurrentUserBookmarks();
-        mutateBookmarkInfo();
         mutatePageInfo();
         mutatePageInfo();
       }
       }
       catch (err) {
       catch (err) {
@@ -81,8 +93,7 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode, bookmarkI
       }
       }
     }
     }
   },
   },
-  // eslint-disable-next-line max-len
-  [isOpen, bookmarkFolders, selectedItem, isBookmarked, bookmarkInfo.pageId, toggleBookmarkHandler, mutateCurrentUserBookmarks, mutateBookmarkInfo, mutatePageInfo]);
+  [isOpen, bookmarkFolders, onToggle, selectedItem, isBookmarked, pageId, toggleBookmarkHandler, mutateCurrentUserBookmarks, mutatePageInfo]);
 
 
   const onMenuItemClickHandler = useCallback(async(e, itemId: string) => {
   const onMenuItemClickHandler = useCallback(async(e, itemId: string) => {
     e.stopPropagation();
     e.stopPropagation();
@@ -90,15 +101,15 @@ export const BookmarkFolderMenu: React.FC<{children?: React.ReactNode, bookmarkI
     setSelectedItem(itemId);
     setSelectedItem(itemId);
 
 
     try {
     try {
-      await addBookmarkToFolder(bookmarkInfo.pageId, itemId === 'root' ? null : itemId);
+      await addBookmarkToFolder(pageId, itemId === 'root' ? null : itemId);
       mutateCurrentUserBookmarks();
       mutateCurrentUserBookmarks();
       mutateBookmarkFolders();
       mutateBookmarkFolders();
-      mutateBookmarkInfo();
+      mutatePageInfo();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkInfo.pageId, mutateCurrentUserBookmarks, mutateBookmarkFolders, mutateBookmarkInfo]);
+  }, [pageId, mutateCurrentUserBookmarks, mutateBookmarkFolders, mutatePageInfo]);
 
 
   const renderBookmarkMenuItem = () => {
   const renderBookmarkMenuItem = () => {
     return (
     return (

+ 7 - 5
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -6,11 +6,13 @@ import { useTranslation } from 'next-i18next';
 import { toastSuccess } from '~/client/util/toastr';
 import { toastSuccess } from '~/client/util/toastr';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import { useSWRxUserBookmarks, useSWRxBookmarkInfo, useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
+import {
+  useSWRxUserBookmarks, useSWRMUTxCurrentUserBookmarks,
+} from '~/stores/bookmark';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useIsReadOnlyUser } from '~/stores/context';
 import { useIsReadOnlyUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { usePageDeleteModal } from '~/stores/modal';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRMUTxPageInfo, useSWRxCurrentPage } from '~/stores/page';
 
 
 import { BookmarkFolderItem } from './BookmarkFolderItem';
 import { BookmarkFolderItem } from './BookmarkFolderItem';
 import { BookmarkItem } from './BookmarkItem';
 import { BookmarkItem } from './BookmarkItem';
@@ -37,18 +39,18 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
 
 
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { mutate: mutateBookmarkInfo } = useSWRxBookmarkInfo(currentPage?._id ?? null);
   const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(userId);
   const { data: bookmarkFolders, mutate: mutateBookmarkFolders } = useSWRxBookmarkFolderAndChild(userId);
   const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxUserBookmarks(userId ?? null);
   const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxUserBookmarks(userId ?? null);
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(currentPage?._id ?? null);
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
 
   const bookmarkFolderTreeMutation = useCallback(() => {
   const bookmarkFolderTreeMutation = useCallback(() => {
     mutateUserBookmarks();
     mutateUserBookmarks();
     mutateCurrentUserBookmarks();
     mutateCurrentUserBookmarks();
-    mutateBookmarkInfo();
+    mutatePageInfo();
     mutateBookmarkFolders();
     mutateBookmarkFolders();
-  }, [mutateBookmarkFolders, mutateBookmarkInfo, mutateCurrentUserBookmarks, mutateUserBookmarks]);
+  }, [mutateBookmarkFolders, mutatePageInfo, mutateCurrentUserBookmarks, mutateUserBookmarks]);
 
 
   const onClickDeleteMenuItemHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
   const onClickDeleteMenuItemHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
     const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {

+ 3 - 5
apps/app/src/components/Navbar/SubNavButtons.tsx

@@ -13,7 +13,6 @@ import {
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 
 
-import { useSWRxBookmarkInfo } from '../../stores/bookmark';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import { useSWRxUsersList } from '../../stores/user';
 import { BookmarkButtons } from '../BookmarkButtons';
 import { BookmarkButtons } from '../BookmarkButtons';
@@ -94,8 +93,6 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
 
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
 
-  const { data: bookmarkInfo } = useSWRxBookmarkInfo(pageId);
-
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
 
 
@@ -227,9 +224,10 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
       )}
       )}
       {revisionId != null && (
       {revisionId != null && (
         <BookmarkButtons
         <BookmarkButtons
+          pageId={pageId}
+          isBookmarked={pageInfo.isBookmarked}
+          bookmarkCount={pageInfo.bookmarkCount}
           hideTotalNumber={isCompactMode}
           hideTotalNumber={isCompactMode}
-          bookmarkedUsers={bookmarkInfo?.bookmarkedUsers}
-          bookmarkInfo={bookmarkInfo}
         />
         />
       )}
       )}
       {revisionId != null && !isCompactMode && (
       {revisionId != null && !isCompactMode && (

+ 4 - 4
apps/app/src/components/PageList/PageListItemL.tsx

@@ -24,13 +24,13 @@ import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
-import { useSWRMUTxCurrentUserBookmarks, useSWRxBookmarkInfo } from '~/stores/bookmark';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import {
 import {
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 
 
-import { useSWRxPageInfo } from '../../stores/page';
+import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
 
 
@@ -90,8 +90,8 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
 
 
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
   const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
   const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageData?._id ?? null);
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
-  const { mutate: mutateBookmarkInfo } = useSWRxBookmarkInfo(pageData?._id);
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
   const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
   const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
 
 
@@ -129,7 +129,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
     await bookmarkOperation(_pageId);
     mutateCurrentUserBookmarks();
     mutateCurrentUserBookmarks();
-    mutateBookmarkInfo();
+    mutatePageInfo();
   };
   };
 
 
   const duplicateMenuItemClickHandler = useCallback(() => {
   const duplicateMenuItemClickHandler = useCallback(() => {

+ 4 - 3
apps/app/src/components/Sidebar/PageTree/Item.tsx

@@ -22,8 +22,9 @@ import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnl
 import {
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
-import { useSWRMUTxCurrentUserBookmarks, useSWRxBookmarkInfo } from '~/stores/bookmark';
+import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
+import { useSWRMUTxPageInfo } from '~/stores/page';
 import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { mutatePageTree, useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -125,7 +126,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
 
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
   const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks();
-  const { mutate: mutateBookmarkInfo } = useSWRxBookmarkInfo(page._id ?? null);
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(page._id ?? null);
 
 
   // descendantCount
   // descendantCount
   const { getDescCount } = usePageTreeDescCountMap();
   const { getDescCount } = usePageTreeDescCountMap();
@@ -262,7 +263,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
     await bookmarkOperation(_pageId);
     mutateCurrentUserBookmarks();
     mutateCurrentUserBookmarks();
-    mutateBookmarkInfo();
+    mutatePageInfo();
   };
   };
 
 
   const duplicateMenuItemClickHandler = useCallback((): void => {
   const duplicateMenuItemClickHandler = useCallback((): void => {

+ 7 - 12
apps/app/src/stores/bookmark.ts

@@ -1,6 +1,8 @@
-import { SWRResponse } from 'swr';
+import { IUser } from '@growi/core';
+import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
-import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
+import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
+
 
 
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 
 
@@ -9,17 +11,10 @@ import { IBookmarkInfo } from '../interfaces/bookmark-info';
 
 
 import { useCurrentUser } from './context';
 import { useCurrentUser } from './context';
 
 
-export const useSWRxBookmarkInfo = (pageId: string | null): SWRResponse<IBookmarkInfo, Error> => {
-  return useSWRImmutable(
+export const useSWRxBookmarkedUsers = (pageId: string | null): SWRResponse<IUser[], Error> => {
+  return useSWR(
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return {
-        sumOfBookmarks: response.data.sumOfBookmarks,
-        isBookmarked: response.data.isBookmarked,
-        bookmarkedUsers: response.data.bookmarkedUsers,
-        pageId: response.data.pageId,
-      };
-    }),
+    endpoint => apiv3Get<IBookmarkInfo>(endpoint).then(response => response.data.bookmarkedUsers),
   );
   );
 };
 };
 
 

+ 25 - 7
apps/app/src/stores/page.tsx

@@ -4,20 +4,20 @@ import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
 } from '@growi/core';
 } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
-import useSWR, { mutate, SWRResponse } from 'swr';
+import useSWR, { mutate, type SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
-import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
-import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
+import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
+import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
-import {
+import type {
   IPageInfo, IPageInfoForOperation,
   IPageInfo, IPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
-import { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
-import { IRevision, IRevisionHasId } from '~/interfaces/revision';
+import type { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
+import type { IRevision, IRevisionHasId } from '~/interfaces/revision';
 
 
-import { IPageTagsInfo } from '../interfaces/tag';
+import type { IPageTagsInfo } from '../interfaces/tag';
 
 
 import {
 import {
   useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
   useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
@@ -157,6 +157,24 @@ export const useSWRxPageInfo = (
   return swrResult;
   return swrResult;
 };
 };
 
 
+export const useSWRMUTxPageInfo = (
+    pageId: string | null | undefined,
+    shareLinkId?: string | null,
+): SWRMutationResponse<IPageInfo | IPageInfoForOperation> => {
+
+  // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
+  const fixedShareLinkId = shareLinkId ?? null;
+
+  const key = useMemo(() => {
+    return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
+  }, [fixedShareLinkId, pageId]);
+
+  return useSWRMutation(
+    key,
+    ([endpoint, pageId, shareLinkId]: [string, string, string|null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+  );
+};
+
 export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>): SWRResponse<IRevisionHasId> => {
 export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>): SWRResponse<IRevisionHasId> => {
   const key = [`/revisions/${revisionId}`, pageId, revisionId];
   const key = [`/revisions/${revisionId}`, pageId, revisionId];
   return useSWRImmutable(
   return useSWRImmutable(