Просмотр исходного кода

Merge pull request #4475 from weseek/master

Release v4.4.9
Yuki Takei 4 лет назад
Родитель
Сommit
4973299c2c
28 измененных файлов с 710 добавлено и 160 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 3 0
      packages/app/config/webpack.common.js
  4. 7 7
      packages/app/package.json
  5. 1 0
      packages/app/resource/locales/en_US/translation.json
  6. 7 1
      packages/app/resource/locales/ja_JP/admin/admin.json
  7. 7 1
      packages/app/resource/locales/zh_CN/admin/admin.json
  8. 56 25
      packages/app/src/client/services/PageContainer.js
  9. 6 0
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  10. 35 13
      packages/app/src/components/LikeButtons.jsx
  11. 5 6
      packages/app/src/components/Navbar/SubNavButtons.jsx
  12. 4 2
      packages/app/src/components/User/SeenUserInfo.jsx
  13. 15 6
      packages/app/src/migrations/20200901034313-update-mail-transmission.js
  14. 0 33
      packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js
  15. 19 48
      packages/app/src/server/routes/apiv3/page.js
  16. 1 3
      packages/app/src/server/service/slack-command-handler/help.js
  17. 0 4
      packages/app/src/server/views/widget/page_content.html
  18. 115 0
      packages/app/src/styles/theme/blackboard.scss
  19. 209 0
      packages/app/src/styles/theme/fire-red.scss
  20. 209 0
      packages/app/src/styles/theme/jade-green.scss
  21. 1 1
      packages/codemirror-textlint/package.json
  22. 1 1
      packages/core/package.json
  23. 1 1
      packages/plugin-attachment-refs/package.json
  24. 1 1
      packages/plugin-lsx/package.json
  25. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  26. 1 1
      packages/slack/package.json
  27. 2 2
      packages/slackbot-proxy/package.json
  28. 1 1
      packages/ui/package.json

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 3 - 0
packages/app/config/webpack.common.js

@@ -45,6 +45,9 @@ module.exports = (options) => {
       'styles/theme-antarctic':       './src/styles/theme/antarctic.scss',
       'styles/theme-spring':          './src/styles/theme/spring.scss',
       'styles/theme-hufflepuff':      './src/styles/theme/hufflepuff.scss',
+      'styles/theme-fire-red':      './src/styles/theme/fire-red.scss',
+      'styles/theme-jade-green':      './src/styles/theme/jade-green.scss',
+      'styles/theme-blackboard':      './src/styles/theme/blackboard.scss',
       // styles for external services
       'styles/style-hackmd':          './src/styles-hackmd/style.scss',
     }, options.entry || {}), // Merge with env dependent settings

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -57,11 +57,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.8",
-    "@growi/plugin-attachment-refs": "^4.4.8",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.8",
-    "@growi/plugin-lsx": "^4.4.8",
-    "@growi/slack": "^4.4.8",
+    "@growi/codemirror-textlint": "^4.4.9-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.9-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.9-RC.0",
+    "@growi/plugin-lsx": "^4.4.9-RC.0",
+    "@growi/slack": "^4.4.9-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
@@ -157,7 +157,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.8",
+    "@growi/ui": "^4.4.9-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 1 - 0
packages/app/resource/locales/en_US/translation.json

@@ -57,6 +57,7 @@
   "Presentation Mode": "Presentation",
   "The end": "The end",
   "Not available for guest": "Not available for guest",
+  "No users have liked this yet.": "No users have liked this yet.",
   "Create Archive Page": "Create Archive Page",
   "File type": "File type",
   "Target page": "Target page",

+ 7 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -11,7 +11,13 @@
     "installed_version": "インストールされているバージョン",
     "list_of_env_vars": "サーバー側で設定されている環境変数一覧",
     "env_var_priority": "セキュリティに関する環境変数を除き、データベースの値が優先的に取得されます。",
-    "about_security": "セキュリティに関する環境変数は <a href='/admin/security'>セキュリティ設定画面</a> からご確認ください。"
+    "about_security": "セキュリティに関する環境変数は <a href='/admin/security'>セキュリティ設定画面</a> からご確認ください。",
+    "copy_prefilled_host_information": {
+      "default": "上記のホスト情報をコピー",
+      "done": "クリップボードにコピーしました!"
+    },
+    "bug_report": "バグを報告する",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>次に GitHub で Issue を投稿してください。</a>"
   },
   "app_setting": {
     "site_name": "サイト名",

+ 7 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -11,7 +11,13 @@
     "installed_version": "已安装版本",
     "list_of_env_vars": "环境变量列表",
     "env_var_priority": "对于安全性以外的环境变量,优先获取数据库的值。",
-    "about_security": "检查安全环境变量的<a href='/admin/security'>安全设置</a>。"
+    "about_security": "检查安全环境变量的<a href='/admin/security'>安全设置</a>。",
+    "copy_prefilled_host_information": {
+      "default": "复制预填的主机信息",
+      "done": "复制到剪贴板!"
+    },
+    "bug_report": "提交一个错误报告",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>然后提交你的问题到GitHub。</a>"
   },
   "app_setting": {
     "site_name": "网站名称 ",

+ 56 - 25
packages/app/src/client/services/PageContainer.js

@@ -51,15 +51,19 @@ export default class PageContainer extends Container {
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
       tocHtml: '',
-      isLiked: false,
+
       isBookmarked: false,
+      sumOfBookmarks: 0,
+
       seenUsers: [],
-      seenUserIds: mainContent.getAttribute('data-page-ids-of-seen-users'),
-      countOfSeenUsers: mainContent.getAttribute('data-page-count-of-seen-users'),
+      seenUserIds: [],
+      sumOfSeenUsers: [],
 
-      likerUsers: [],
+      isLiked: false,
+      likers: [],
+      likerIds: [],
       sumOfLikers: 0,
-      sumOfBookmarks: 0,
+
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
@@ -109,7 +113,7 @@ export default class PageContainer extends Container {
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(appContainer), 900); // process as late as possible
 
     this.initStateMarkdown();
-    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
+    this.checkAndUpdateImageUrlCached(this.state.likers);
 
     const { isSharedUser } = this.appContainer;
 
@@ -117,8 +121,10 @@ export default class PageContainer extends Container {
     const isAbleToGetAttachedInformationAboutPages = this.state.isPageExist && !isSharedUser;
 
     if (isAbleToGetAttachedInformationAboutPages) {
-      this.retrieveSeenUsers();
-      this.retrieveLikeInfo();
+      // We don't retrieve bookmarks in the initial page load
+      // as it is stored in a separate collection to like and seen user
+      // data so it has a separate api endpoint.
+      this.initialPageLoad();
       this.retrieveBookmarkInfo();
     }
 
@@ -219,7 +225,7 @@ export default class PageContainer extends Container {
    * whether to like button
    * not displayed on user page
    */
-  get isAbleToShowLikeButton() {
+  get isAbleToShowLikeButtons() {
     const { isUserPage } = this.state;
     const { isSharedUser } = this.appContainer;
 
@@ -264,29 +270,54 @@ export default class PageContainer extends Container {
     this.state.markdown = markdown;
   }
 
-  async retrieveSeenUsers() {
-    const { users } = await this.appContainer.apiGet('/users.list', { user_ids: this.state.seenUserIds });
 
-    this.setState({ seenUsers: users });
-    this.checkAndUpdateImageUrlCached(users);
-  }
+  async initialPageLoad() {
+    {
+      const {
+        data: {
+          likerIds, sumOfLikers, isLiked, seenUserIds, sumOfSeenUsers, isSeen,
+        },
+      } = await this.appContainer.apiv3Get('/page/info', { _id: this.state.pageId });
 
-  async retrieveLikeInfo() {
-    const res = await this.appContainer.apiv3Get('/page/like-info', { _id: this.state.pageId });
-    const { sumOfLikers, isLiked } = res.data;
+      await this.setState({
+        sumOfLikers,
+        isLiked,
+        likerIds,
+        seenUserIds,
+        sumOfSeenUsers,
+        isSeen,
+      });
+    }
 
-    this.setState({
-      sumOfLikers,
-      isLiked,
-    });
+    await this.retrieveLikersAndSeenUsers();
   }
 
   async toggleLike() {
-    const bool = !this.state.isLiked;
-    await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool });
-    this.setState({ isLiked: bool });
+    {
+      const toggledIsLiked = !this.state.isLiked;
+      await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool: toggledIsLiked });
+
+      await this.setState(state => ({
+        isLiked: toggledIsLiked,
+        sumOfLikers: toggledIsLiked ? state.sumOfLikers + 1 : state.sumOfLikers - 1,
+        likerIds: toggledIsLiked
+          ? [...this.state.likerIds, this.appContainer.currentUserId]
+          : state.likerIds.filter(id => id !== this.appContainer.currentUserId),
+      }));
+    }
+
+    await this.retrieveLikersAndSeenUsers();
+  }
+
+  async retrieveLikersAndSeenUsers() {
+    const { users } = await this.appContainer.apiGet('/users.list', { user_ids: [...this.state.likerIds, ...this.state.seenUserIds].join(',') });
 
-    return this.retrieveLikeInfo();
+    await this.setState({
+      likers: users.filter(({ id }) => this.state.likerIds.includes(id)).slice(0, 15),
+      seenUsers: users.filter(({ id }) => this.state.seenUserIds.includes(id)).slice(0, 15),
+    });
+
+    this.checkAndUpdateImageUrlCached(users);
   }
 
   async retrieveBookmarkInfo() {

+ 6 - 0
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -22,6 +22,12 @@ class CustomizeThemeOptions extends React.Component {
       name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
     }, {
       name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
+    }, {
+      name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
+    }, {
+      name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
+    }, {
+      name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
     }];
 
     const uniqueTheme = [{

+ 35 - 13
packages/app/src/components/LikeButton.jsx → packages/app/src/components/LikeButtons.jsx

@@ -1,22 +1,35 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { UncontrolledTooltip } from 'reactstrap';
+import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
+import UserPictureList from './User/UserPictureList';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 import { toastError } from '~/client/util/apiNotification';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 
-class LikeButton extends React.Component {
+class LikeButtons extends React.Component {
 
   constructor(props) {
     super(props);
 
+    this.state = {
+      isPopoverOpen: false,
+    };
+
+    this.togglePopover = this.togglePopover.bind(this);
     this.handleClick = this.handleClick.bind(this);
   }
 
+  togglePopover() {
+    this.setState(prevState => ({
+      ...prevState,
+      isPopoverOpen: !prevState.isPopoverOpen,
+    }));
+  }
+
   async handleClick() {
     const { appContainer, pageContainer } = this.props;
     const { isGuestUser } = appContainer;
@@ -33,31 +46,40 @@ class LikeButton extends React.Component {
     }
   }
 
-
   render() {
     const { appContainer, pageContainer, t } = this.props;
     const { isGuestUser } = appContainer;
+    const {
+      state: { likers, sumOfLikers, isLiked },
+    } = pageContainer;
 
     return (
-      <div>
+      <div className="btn-group" role="group" aria-label="Like buttons">
         <button
           type="button"
           id="like-button"
           onClick={this.handleClick}
           className={`btn btn-like border-0
-          ${pageContainer.state.isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+            ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
-          <i className="icon-like mr-3"></i>
-          <span className="total-likes">
-            {pageContainer.state.sumOfLikers}
-          </span>
+          <i className="icon-like"></i>
         </button>
-
         {isGuestUser && (
           <UncontrolledTooltip placement="top" target="like-button" fade={false}>
             {t('Not available for guest')}
           </UncontrolledTooltip>
         )}
+
+        <button type="button" id="po-total-likes" className={`btn btn-like border-0 total-likes ${isLiked ? 'active' : ''}`}>
+          {sumOfLikers}
+        </button>
+        <Popover placement="bottom" isOpen={this.state.isPopoverOpen} target="po-total-likes" toggle={this.togglePopover} trigger="legacy">
+          <PopoverBody className="seen-user-popover">
+            <div className="px-2 text-right user-list-content text-truncate text-muted">
+              {likers.length ? <UserPictureList users={likers} /> : t('No users have liked this yet.')}
+            </div>
+          </PopoverBody>
+        </Popover>
       </div>
     );
   }
@@ -67,9 +89,9 @@ class LikeButton extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const LikeButtonWrapper = withUnstatedContainers(LikeButton, [AppContainer, PageContainer]);
+const LikeButtonsWrapper = withUnstatedContainers(LikeButtons, [AppContainer, PageContainer]);
 
-LikeButton.propTypes = {
+LikeButtons.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
@@ -77,4 +99,4 @@ LikeButton.propTypes = {
   size: PropTypes.string,
 };
 
-export default withTranslation()(LikeButtonWrapper);
+export default withTranslation()(LikeButtonsWrapper);

+ 5 - 6
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -6,7 +6,7 @@ import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 import BookmarkButton from '../BookmarkButton';
-import LikeButton from '../LikeButton';
+import LikeButtons from '../LikeButtons';
 import PageManagement from '../Page/PageManagement';
 
 const SubnavButtons = (props) => {
@@ -21,15 +21,14 @@ const SubnavButtons = (props) => {
 
     return (
       <>
-        {pageContainer.isAbleToShowLikeButton && (
+        {pageContainer.isAbleToShowLikeButtons && (
           <span>
-            <LikeButton />
+            <LikeButtons />
           </span>
         )}
         <span>
           <BookmarkButton />
         </span>
-
       </>
     );
   };
@@ -42,8 +41,8 @@ const SubnavButtons = (props) => {
     <>
       {isViewMode && (
         <>
-          { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
-          { pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} /> }
+          {pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} />}
+          {pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} />}
         </>
       )}
     </>

+ 4 - 2
packages/app/src/components/User/SeenUserInfo.jsx

@@ -22,8 +22,10 @@ const SeenUserInfo = (props) => {
   return (
     <div className="grw-seen-user-info">
       <Button id="po-seen-user" color="link" className="px-2">
-        <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
-        <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
+        <span className="mr-1 footstamp-icon">
+          <FootstampIcon />
+        </span>
+        <span className="seen-user-count">{pageContainer.state.sumOfSeenUsers}</span>
       </Button>
       <Popover placement="bottom" isOpen={popoverOpen} target="po-seen-user" toggle={toggle} trigger="legacy" disabled={disabled}>
         <PopoverBody className="seen-user-popover">

+ 15 - 6
packages/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -11,17 +11,26 @@ module.exports = {
     logger.info('Apply migration');
     mongoose.connect(getMongoUri(), mongoOptions);
 
-    const sesExist = await Config.findOne({
+    const sesAccessKeyId = await Config.findOne({
       ns: 'crowi',
       key: 'mail:sesAccessKeyId',
     });
+    const transmissionMethod = await Config.findOne({
+      ns: 'crowi',
+      key: 'mail:transmissionMethod',
+    });
 
-    if (sesExist == null) {
-      return logger.info('Document does not exist, value of transmission method will be set smtp automatically.');
+    if (sesAccessKeyId == null) {
+      return logger.info('The key \'mail:sesAccessKeyId\' does not exist, value of transmission method will be set smtp automatically.');
     }
-    const value = (
-      sesExist.value != null ? 'ses' : 'smtp'
-    );
+    if (transmissionMethod != null) {
+      return logger.info('The key \'mail:transmissionMethod\' already exists, there is no need to migrate.');
+    }
+
+    const value = sesAccessKeyId.value != null
+      ? JSON.stringify('ses')
+      : JSON.stringify('smtp');
+
     await Config.create({
       ns: 'crowi',
       key: 'mail:transmissionMethod',

+ 0 - 33
packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js

@@ -1,33 +0,0 @@
-import { getMongoUri, mongoOptions } from '@growi/core';
-import loggerFactory from '~/utils/logger';
-
-import Config from '~/server/models/config';
-
-const logger = loggerFactory('growi:migrate:update-mail-transmission-fix');
-
-const mongoose = require('mongoose');
-
-module.exports = {
-  async up(db, client) {
-    logger.info('Apply migration');
-    mongoose.connect(getMongoUri(), mongoOptions);
-
-    const transmissionMethod = await Config.findOne({
-      ns: 'crowi',
-      key: 'mail:transmissionMethod',
-    });
-
-    if (transmissionMethod == null) {
-      return logger.info('No need to change.');
-    }
-
-    transmissionMethod.value = JSON.stringify(transmissionMethod.value);
-    await transmissionMethod.save();
-
-    logger.info('Migration has successfully applied');
-  },
-
-  async down(db, client) {
-    // do not rollback
-  },
-};

+ 19 - 48
packages/app/src/server/routes/apiv3/page.js

@@ -112,17 +112,6 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          bool:
  *            type: boolean
  *            description: boolean for like status
- *
- *      LikeInfo:
- *        description: LikeInfo
- *        type: object
- *        properties:
- *          sumOfLikers:
- *            type: number
- *            description: how many people liked the page
- *          isLiked:
- *            type: boolean
- *            description: Whether the request user liked (will be returned if the user is included in the request)
  */
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
@@ -141,9 +130,6 @@ module.exports = (crowi) => {
       body('pageId').isString(),
       body('bool').isBoolean(),
     ],
-    likeInfo: [
-      query('_id').isMongoId(),
-    ],
     export: [
       query('format').isString().isIn(['md', 'pdf']),
       query('revisionId').isString(),
@@ -222,50 +208,35 @@ module.exports = (crowi) => {
     }
   });
 
-  /**
-   * @swagger
-   *
-   *    /page/like-info:
-   *      get:
-   *        tags: [Page]
-   *        summary: /page/like-info
-   *        description: Get like info
-   *        operationId: getLikeInfo
-   *        parameters:
-   *          - name: _id
-   *            in: query
-   *            description: page id
-   *            schema:
-   *              type: string
-   *        responses:
-   *          200:
-   *            description: Succeeded to get bookmark info.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/LikeInfo'
-   */
-  router.get('/like-info', loginRequired, validator.likeInfo, apiV3FormValidator, async(req, res) => {
-    const pageId = req.query._id;
-
-    const responsesParams = {};
+  router.get(('/info', loginRequired), async(req, res) => {
 
     try {
+      const pageId = req.query._id;
       const page = await Page.findById(pageId);
-      responsesParams.sumOfLikers = page.liker.length;
 
-      // guest user return nothing
-      if (!req.user) {
-        return res.apiv3(responsesParams);
+      const guestUserResponse = {
+        sumOfLikers: page.liker.length,
+        likerIds: page.liker.slice(0, 15),
+        seenUserIds: page.seenUsers.slice(0, 15),
+        sumOfSeenUsers: page.seenUsers.length,
+        isSeen: page.seenUsers.length > 0,
+      };
+
+      {
+        const isGuestUser = !req.user;
+        if (isGuestUser) {
+          return res.apiv3(guestUserResponse);
+        }
       }
 
-      responsesParams.isLiked = page.liker.includes(req.user._id);
-      return res.apiv3(responsesParams);
+      const userResponse = { ...guestUserResponse, isLiked: page.isLiked(req.user) };
+      return res.apiv3(userResponse);
     }
     catch (err) {
-      logger.error('get-like-count-failed', err);
+      logger.error('get-page-info', err);
       return res.apiv3Err(err, 500);
     }
+
   });
 
   /**

+ 1 - 3
packages/app/src/server/service/slack-command-handler/help.js

@@ -13,9 +13,7 @@ module.exports = (crowi) => {
     const appTitle = crowi.appService.getAppTitle();
     const appSiteUrl = crowi.appService.getSiteUrl();
     // adjust spacing
-    let message = '*Help*\n\n';
-    message += `GROWI App Title: *${appTitle}*`;
-    message += `GROWI Url: ${appSiteUrl}`;
+    let message = `*Help* (*${appTitle}* at ${appSiteUrl})\n\n`;
     message += 'Usage:     `/growi [command] [args]`\n\n';
     message += 'Commands:\n\n';
     message += '`/growi note`                          Take a note on GROWI\n\n';

+ 0 - 4
packages/app/src/server/views/widget/page_content.html

@@ -12,8 +12,6 @@
   data-page-grant="{{ grant }}"
   data-page-grant-group="{{ grantedGroupId }}"
   data-page-grant-group-name="{{ grantedGroupName }}"
-  data-page-is-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
-  data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
   data-page-is-not-creatable="false"
@@ -27,8 +25,6 @@
   data-page-deleted-at="{% if page && page.deletedAt %}{{ page.deletedAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-has-children="{% if pages.length > 0 %}true{% else %}false{% endif %}"
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"
-  data-page-ids-of-seen-users="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
-  data-page-count-of-seen-users="{{ page.seenUsers.length|default(0) }}"
   data-share-links-number="{% if page %}{{ sharelinksNumber }}{% endif %}"
   data-share-link-id="{% if sharelink %}{{ sharelink._id|json }}{% endif %}"
   >

+ 115 - 0
packages/app/src/styles/theme/blackboard.scss

@@ -0,0 +1,115 @@
+@import '../variables';
+@import '../override-bootstrap-variables';
+
+html[light],
+html[dark] {
+  // Theme colors
+  $themecolor: #da8506;
+  $themelight: #223729;
+  $accentcolor: #739aff;
+  $subthemecolor: #192a1f;
+
+  $primary: $themecolor;
+  $dark: #223729;
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-navbar: #563e23;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: darken($themelight, 5%);
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+  // Font colors
+  $color-global: #ffffff;
+  $color-reversal: $gray-100;
+  $color-link: $accentcolor;
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: $accentcolor;
+  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $subthemecolor;
+  $color-inline-code: #c7254e; // optional
+  $color-search: $dark;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $accentcolor;
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: #563e23;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #bebebe 0%, #d8d8d8 100%);
+
+  // Logo colors
+  $bgcolor-logo: $color-global;
+  $fillcolor-logo-mark: $color-global;
+  // $fillcolor-logo-mark: #4e5a60;
+
+  // Sidebar
+  $bgcolor-sidebar: #7b5932;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: $subthemecolor;
+  $color-sidebar-context: $color-global;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: $color-global;
+  $bordercolor-inline-code: #4d4d4d; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-dark';
+
+  // Navs
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        color: $color-link;
+        background-color: transparent;
+        border-color: $border-color-theme;
+      }
+    }
+  }
+
+  // Table
+  .table {
+    color: white;
+    background-color: $themelight;
+    border-color: $border-color-theme;
+  }
+
+  // Button
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+    }
+  }
+}

+ 209 - 0
packages/app/src/styles/theme/fire-red.scss

@@ -0,0 +1,209 @@
+@import '../variables';
+@import '../override-bootstrap-variables';
+
+html[light] {
+  // Theme colors
+  $themecolor: #ea5532;
+  $themelight: #ffffff;
+  $accentcolor: #bfbfbf;
+  $subthemecolor: #e6e6e6;
+
+  $primary: $themecolor;
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $accentcolor;
+  $bgcolor-blinked-section: rgba($primary, 0.1);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+  // Font colors
+  $color-global: #2c2c2c;
+  $color-reversal: $gray-100;
+  $color-link: $primary;
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: $primary;
+  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e; // optional
+  $color-search: $color-global;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $color-search;
+  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: $color-global;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+  // Logo colors
+  $bgcolor-logo: $themelight;
+  $fillcolor-logo-mark: $themelight;
+
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
+  // Sidebar resize button
+  $color-resize-button: #ffffff;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: #ebebeb;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: $primary;
+  $bordercolor-inline-code: #ccc8c8; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  // Navs {
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        background-color: transparent;
+      }
+    }
+  }
+  // Button
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+    }
+  }
+}
+
+html[dark] {
+  // Theme colors
+  $themecolor: #ea5532;
+  $themedark: #333333;
+  $accentcolor: #212121;
+  $subthemecolor: #2e2e2e;
+
+  $primary: #ea5532;
+  $dark: #a7a7a7;
+
+  // Background colors
+  $bgcolor-global: $themedark;
+  $bgcolor-navbar: #2b2b2b;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: darken($themedark, 5%);
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+  // Font colors
+  $color-global: #ffffff;
+  $color-reversal: $gray-100;
+  $color-link: $primary;
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: $primary;
+  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $subthemecolor;
+  $color-inline-code: #c7254e; // optional
+  $color-search: $dark;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $accentcolor;
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: #2c2c2c;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #ea5532 0%, #c9171e 100%);
+
+  // Logo colors
+  $bgcolor-logo: #ffffff;
+  $fillcolor-logo-mark: #ffffff;
+  // $fillcolor-logo-mark: #4e5a60;
+
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: #2e2e2e;
+  $color-sidebar-context: $color-global;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: $primary;
+  $bordercolor-inline-code: #4d4d4d; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-dark';
+
+  // Navs
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        color: $color-link;
+        background-color: transparent;
+        border-color: $border-color-theme;
+      }
+    }
+  }
+
+  // Table
+  .table {
+    color: white;
+  }
+
+  // Button
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+    }
+  }
+}

+ 209 - 0
packages/app/src/styles/theme/jade-green.scss

@@ -0,0 +1,209 @@
+@import '../variables';
+@import '../override-bootstrap-variables';
+
+html[light] {
+  // Theme colors
+  $themecolor: #38b48b;
+  $themelight: #ffffff;
+  $accentcolor: #bfbfbf;
+  $subthemecolor: #e6e6e6;
+
+  $primary: $themecolor;
+
+  // Background colors
+  $bgcolor-global: $themelight;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $accentcolor;
+  $bgcolor-blinked-section: rgba($primary, 0.1);
+  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+  // Font colors
+  $color-global: #2c2c2c;
+  $color-reversal: $gray-100;
+  $color-link: $primary;
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: $primary;
+  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e; // optional
+  $color-search: $color-global;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $color-search;
+  $bgcolor-list-hover: darken($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: $color-global;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+  // Logo colors
+  $bgcolor-logo: $themelight;
+  $fillcolor-logo-mark: $themelight;
+
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
+  // Sidebar resize button
+  $color-resize-button: #ffffff;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: #ebebeb;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: $primary;
+  $bordercolor-inline-code: #ccc8c8; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  // Navs {
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        background-color: transparent;
+      }
+    }
+  }
+  // Button
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+    }
+  }
+}
+
+html[dark] {
+  // Theme colors
+  $themecolor: #38b48b;
+  $themedark: #333333;
+  $accentcolor: #212121;
+  $subthemecolor: #2e2e2e;
+
+  $primary: #38b48b;
+  $dark: #a7a7a7;
+
+  // Background colors
+  $bgcolor-global: $themedark;
+  $bgcolor-navbar: #2b2b2b;
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: darken($themedark, 5%);
+  $bgcolor-blinked-section: rgba($primary, 0.5);
+  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+  // Font colors
+  $color-global: #ffffff;
+  $color-reversal: $gray-100;
+  $color-link: $primary;
+  $color-link-hover: lighten($color-link, 12%);
+  $color-link-wiki: $primary;
+  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $subthemecolor;
+  $color-inline-code: #c7254e; // optional
+  $color-search: $dark;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $accentcolor;
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: #2c2c2c;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+  // Logo colors
+  $bgcolor-logo: #ffffff;
+  $fillcolor-logo-mark: #ffffff;
+  // $fillcolor-logo-mark: #4e5a60;
+
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $primary;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: #2e2e2e;
+  $color-sidebar-context: $color-global;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: $primary;
+  $bordercolor-inline-code: #4d4d4d; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-dark';
+
+  // Navs
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        color: $color-link;
+        background-color: transparent;
+        border-color: $border-color-theme;
+      }
+    }
+  }
+
+  // Table
+  .table {
+    color: white;
+  }
+
+  // Button
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+    }
+  }
+}

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "4.4.8",
+  "version": "4.4.9-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^4.4.8",
+    "@growi/slack": "^4.4.9-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "4.4.8",
+  "version": "4.4.9-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [