Parcourir la source

Merge branch 'feat/77830-save-subscriptioin-model' into feat/77832-get-subscription-on-client-side

Shun Miyazawa il y a 4 ans
Parent
commit
8b7445490e

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -110,7 +110,7 @@ const GrowiSubNavigation = (props) => {
 
 
         <div className="d-flex flex-column align-items-end">
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
           <div className="d-flex">
-            <SubnavButtons isCompactMode={isCompactMode} />
+            <SubnavButtons isCompactMode={isCompactMode} pageId={pageId} />
           </div>
           </div>
           <div className="mt-2">
           <div className="mt-2">
             {pageContainer.isAbleToShowPageEditorModeManager && (
             {pageContainer.isAbleToShowPageEditorModeManager && (

+ 3 - 2
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -12,7 +12,7 @@ import PageManagement from '../Page/PageManagement';
 
 
 const SubnavButtons = (props) => {
 const SubnavButtons = (props) => {
   const {
   const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
+    appContainer, navigationContainer, pageContainer, isCompactMode, pageId,
   } = props;
   } = props;
 
 
   /* eslint-enable react/prop-types */
   /* eslint-enable react/prop-types */
@@ -23,7 +23,7 @@ const SubnavButtons = (props) => {
     return (
     return (
       <>
       <>
         <span>
         <span>
-          <SubscribeButton />
+          <SubscribeButton pageId={pageId} />
         </span>
         </span>
         {pageContainer.isAbleToShowLikeButton && (
         {pageContainer.isAbleToShowLikeButton && (
           <span>
           <span>
@@ -66,6 +66,7 @@ SubnavButtons.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isCompactMode: PropTypes.bool,
   isCompactMode: PropTypes.bool,
+  pageId: PropTypes.string,
 };
 };
 
 
 export default SubnavButtonsWrapper;
 export default SubnavButtonsWrapper;

+ 0 - 66
packages/app/src/components/SubscribeButton.jsx

@@ -1,66 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import { toastError } from '~/client/util/apiNotification';
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
-
-const SubscruibeButton = (props) => {
-
-  const [isWatching, setIsWatching] = useState(true);
-
-  const { appContainer, pageContainer } = props;
-
-  const handleClick = async() => {
-    try {
-      const res = await appContainer.apiv3Put('page/subscribe', { pageId: pageContainer.state.pageId, status: !isWatching });
-      if (res) {
-        const { subscription } = res.data;
-        setIsWatching(subscription.status === 'WATCH');
-      }
-    }
-    catch (err) {
-      toastError(err);
-    }
-  };
-
-  return (
-    <>
-      <button
-        type="button"
-        id="subscribe-button"
-        onClick={handleClick}
-        className={`btn btn-watch border-0 ${`btn-${props.size}`} ${isWatching ? 'active' : ''} `}
-      >
-        {isWatching && (
-          <i className="fa fa-eye"></i>
-        )}
-
-        {!isWatching && (
-          <i className="fa fa-eye-slash"></i>
-        )}
-      </button>
-    </>
-  );
-
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SubscruibeButtonWrapper = withUnstatedContainers(SubscruibeButton, [AppContainer, PageContainer]);
-
-SubscruibeButton.propTypes = {
-  size: PropTypes.string,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-SubscruibeButton.defaultProps = {
-  size: 'md',
-};
-
-export default withTranslation()(SubscruibeButtonWrapper);

+ 64 - 0
packages/app/src/components/SubscribeButton.tsx

@@ -0,0 +1,64 @@
+import React, { useState, FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+import { withUnstatedContainers } from './UnstatedUtils';
+
+import { toastError } from '~/client/util/apiNotification';
+import AppContainer from '~/client/services/AppContainer';
+import PageContainer from '~/client/services/PageContainer';
+
+type Props = {
+  appContainer: AppContainer,
+  pageId: string,
+};
+
+const SubscruibeButton: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
+  const { appContainer, pageId } = props;
+  const [isSubscribing, setIsSubscribing] = useState(false);
+
+  const handleClick = async() => {
+    if (appContainer.isGuestUser) {
+      return;
+    }
+
+    try {
+      const res = await appContainer.apiv3Put('page/subscribe', { pageId, status: !isSubscribing });
+      if (res) {
+        const { subscription } = res.data;
+        setIsSubscribing(subscription.status === 'SUBSCRIBE');
+      }
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <>
+      <button
+        type="button"
+        id="subscribe-button"
+        onClick={handleClick}
+        className={`btn btn-subscribe border-0 ${isSubscribing ? 'active' : ''}  ${appContainer.isGuestUser ? 'disabled' : ''}`}
+      >
+        <i className={isSubscribing ? 'fa fa-eye' : 'fa fa-eye-slash'}></i>
+      </button>
+
+      {appContainer.isGuestUser && (
+        <UncontrolledTooltip placement="top" target="subscribe-button" fade={false}>
+          {t('Not available for guest')}
+        </UncontrolledTooltip>
+      )}
+    </>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SubscruibeButtonWrapper = withUnstatedContainers(SubscruibeButton, [AppContainer, PageContainer]);
+export default SubscruibeButtonWrapper;

+ 4 - 4
packages/app/src/server/models/activity.ts

@@ -182,10 +182,10 @@ module.exports = function(crowi: Crowi) {
     const { user: actionUser, targetModel, target } = this;
     const { user: actionUser, targetModel, target } = this;
 
 
     const model: any = await this.model(targetModel).findById(target);
     const model: any = await this.model(targetModel).findById(target);
-    const [targetUsers, watchUsers, ignoreUsers] = await Promise.all([
+    const [targetUsers, subscribeUsers, unsubscribeUsers] = await Promise.all([
       model.getNotificationTargetUsers(),
       model.getNotificationTargetUsers(),
-      Subscription.getWatchers((target as any) as Types.ObjectId),
-      Subscription.getUnwatchers((target as any) as Types.ObjectId),
+      Subscription.getSubscription((target as any) as Types.ObjectId),
+      Subscription.getUnsubscription((target as any) as Types.ObjectId),
     ]);
     ]);
 
 
     const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
     const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
@@ -193,7 +193,7 @@ module.exports = function(crowi: Crowi) {
       const ids = pull.map(object => object.toString());
       const ids = pull.map(object => object.toString());
       return array.filter(object => !ids.includes(object.toString()));
       return array.filter(object => !ids.includes(object.toString()));
     };
     };
-    const notificationUsers = filter(unique([...targetUsers, ...watchUsers]), [...ignoreUsers, actionUser]);
+    const notificationUsers = filter(unique([...targetUsers, ...subscribeUsers]), [...unsubscribeUsers, actionUser]);
     const activeNotificationUsers = await User.find({
     const activeNotificationUsers = await User.find({
       _id: { $in: notificationUsers },
       _id: { $in: notificationUsers },
       status: User.STATUS_ACTIVE,
       status: User.STATUS_ACTIVE,

+ 27 - 26
packages/app/src/server/models/subscription.ts

@@ -5,9 +5,9 @@ import {
 import ActivityDefine from '../util/activityDefine';
 import ActivityDefine from '../util/activityDefine';
 import { getOrCreateModel } from '../util/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-const STATUS_WATCH = 'WATCH';
-const STATUS_UNWATCH = 'UNWATCH';
-const STATUSES = [STATUS_WATCH, STATUS_UNWATCH];
+const STATUS_SUBSCRIBE = 'SUBSCRIBE';
+const STATUS_UNSUBSCRIBE = 'UNSUBSCRIBE';
+const STATUSES = [STATUS_SUBSCRIBE, STATUS_UNSUBSCRIBE];
 
 
 export interface ISubscription {
 export interface ISubscription {
   user: Types.ObjectId
   user: Types.ObjectId
@@ -16,20 +16,21 @@ export interface ISubscription {
   status: string
   status: string
   createdAt: Date
   createdAt: Date
 
 
-  isWatching(): boolean
-  isUnwatching(): boolean
+  isSubscribing(): boolean
+  isUnsubscribing(): boolean
 }
 }
 
 
 export interface SubscriptionDocument extends ISubscription, Document {}
 export interface SubscriptionDocument extends ISubscription, Document {}
 
 
 export interface SubscriptionModel extends Model<SubscriptionDocument> {
 export interface SubscriptionModel extends Model<SubscriptionDocument> {
   findByUserIdAndTargetId(userId: Types.ObjectId, targetId: Types.ObjectId): any
   findByUserIdAndTargetId(userId: Types.ObjectId, targetId: Types.ObjectId): any
-  upsertWatcher(user: Types.ObjectId, targetModel: string, target: Types.ObjectId, status: string): any
-  watchByPageId(user: Types.ObjectId, pageId: Types.ObjectId, status: string): any
-  getWatchers(target: Types.ObjectId): Promise<Types.ObjectId[]>
-  getUnwatchers(target: Types.ObjectId): Promise<Types.ObjectId[]>
-  STATUS_WATCH(): string
-  STATUS_UNWATCH(): string
+  upsertSubscription(user: Types.ObjectId, targetModel: string, target: Types.ObjectId, status: string): any
+  subscribeByPageId(user: Types.ObjectId, pageId: Types.ObjectId, status: string): any
+  getSubscription(target: Types.ObjectId): Promise<Types.ObjectId[]>
+  getUnsubscription(target: Types.ObjectId): Promise<Types.ObjectId[]>
+
+  STATUS_SUBSCRIBE: string
+  STATUS_UNSUBSCRIB: string
 }
 }
 
 
 const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
 const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
@@ -57,19 +58,19 @@ const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
   createdAt: { type: Date, default: Date.now },
   createdAt: { type: Date, default: Date.now },
 });
 });
 
 
-subscriptionSchema.methods.isWatching = function() {
-  return this.status === STATUS_WATCH;
+subscriptionSchema.methods.isSubscribing = function() {
+  return this.status === STATUS_SUBSCRIBE;
 };
 };
 
 
-subscriptionSchema.methods.isUnwatching = function() {
-  return this.status === STATUS_UNWATCH;
+subscriptionSchema.methods.isUnsubscribing = function() {
+  return this.status === STATUS_UNSUBSCRIBE;
 };
 };
 
 
 subscriptionSchema.statics.findByUserIdAndTargetId = function(userId, targetId) {
 subscriptionSchema.statics.findByUserIdAndTargetId = function(userId, targetId) {
   return this.findOne({ user: userId, target: targetId });
   return this.findOne({ user: userId, target: targetId });
 };
 };
 
 
-subscriptionSchema.statics.upsertWatcher = function(user, targetModel, target, status) {
+subscriptionSchema.statics.upsertSubscription = function(user, targetModel, target, status) {
   const query = { user, targetModel, target };
   const query = { user, targetModel, target };
   const doc = { ...query, status };
   const doc = { ...query, status };
   const options = {
   const options = {
@@ -78,24 +79,24 @@ subscriptionSchema.statics.upsertWatcher = function(user, targetModel, target, s
   return this.findOneAndUpdate(query, doc, options);
   return this.findOneAndUpdate(query, doc, options);
 };
 };
 
 
-subscriptionSchema.statics.watchByPageId = function(user, pageId, status) {
-  return this.upsertWatcher(user, 'Page', pageId, status);
+subscriptionSchema.statics.subscribeByPageId = function(user, pageId, status) {
+  return this.upsertSubscription(user, 'Page', pageId, status);
 };
 };
 
 
-subscriptionSchema.statics.getWatchers = async function(target) {
-  return this.find({ target, status: STATUS_WATCH }).distinct('user');
+subscriptionSchema.statics.getSubscription = async function(target) {
+  return this.find({ target, status: STATUS_SUBSCRIBE }).distinct('user');
 };
 };
 
 
-subscriptionSchema.statics.getUnwatchers = async function(target) {
-  return this.find({ target, status: STATUS_UNWATCH }).distinct('user');
+subscriptionSchema.statics.getUnsubscription = async function(target) {
+  return this.find({ target, status: STATUS_UNSUBSCRIBE }).distinct('user');
 };
 };
 
 
-subscriptionSchema.statics.STATUS_WATCH = function() {
-  return STATUS_WATCH;
+subscriptionSchema.statics.STATUS_SUBSCRIBE = function() {
+  return STATUS_SUBSCRIBE;
 };
 };
 
 
-subscriptionSchema.statics.STATUS_UNWATCH = function() {
-  return STATUS_UNWATCH;
+subscriptionSchema.statics.STATUS_UNSUBSCRIBE = function() {
+  return STATUS_UNSUBSCRIBE;
 };
 };
 
 
 export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>('Subscription', subscriptionSchema);
 export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>('Subscription', subscriptionSchema);

+ 7 - 3
packages/app/src/server/routes/apiv3/page.js

@@ -162,6 +162,10 @@ module.exports = (crowi) => {
       query('fromPath').isString(),
       query('fromPath').isString(),
       query('toPath').isString(),
       query('toPath').isString(),
     ],
     ],
+    subscribe: [
+      body('pageId').isString(),
+      body('status').isBoolean(),
+    ],
   };
   };
 
 
   /**
   /**
@@ -490,12 +494,12 @@ module.exports = (crowi) => {
    *          500:
    *          500:
    *            description: Internal server error.
    *            description: Internal server error.
    */
    */
-  router.put('/subscribe', accessTokenParser, loginRequiredStrictly, csrf, async(req, res) => {
+  router.put('/subscribe', accessTokenParser, loginRequiredStrictly, csrf, validator.subscribe, apiV3FormValidator, async(req, res) => {
     const { pageId } = req.body;
     const { pageId } = req.body;
     const userId = req.user._id;
     const userId = req.user._id;
-    const status = req.body.status ? Subscription.STATUS_WATCH() : Subscription.STATUS_UNWATCH();
+    const status = req.body.status ? Subscription.STATUS_SUBSCRIBE() : Subscription.STATUS_UNSUBSCRIBE();
     try {
     try {
-      const subscription = await Subscription.watchByPageId(userId, pageId, status);
+      const subscription = await Subscription.subscribeByPageId(userId, pageId, status);
       return res.apiv3({ subscription });
       return res.apiv3({ subscription });
     }
     }
     catch (err) {
     catch (err) {

+ 2 - 2
packages/app/src/styles/_subnav.scss

@@ -38,9 +38,9 @@
     }
     }
   }
   }
 
 
-  .btn-watch,
   .btn-like,
   .btn-like,
-  .btn-bookmark {
+  .btn-bookmark,
+  .btn-subscribe {
     height: 40px;
     height: 40px;
     font-size: 20px;
     font-size: 20px;
     border-radius: $border-radius-xl;
     border-radius: $border-radius-xl;

+ 1 - 1
packages/app/src/styles/atoms/_buttons.scss

@@ -20,7 +20,7 @@
   }
   }
 }
 }
 
 
-.btn.btn-watch {
+.btn.btn-subscribe {
   @include button-outline-variant($secondary, $success, rgba(lighten($success, 10%), 0.15), rgba(lighten($success, 10%), 0.5));
   @include button-outline-variant($secondary, $success, rgba(lighten($success, 10%), 0.15), rgba(lighten($success, 10%), 0.5));
   &:not(:disabled):not(.disabled):active,
   &:not(:disabled):not(.disabled):active,
   &:not(:disabled):not(.disabled).active {
   &:not(:disabled):not(.disabled).active {