mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 19:27:52 +01:00
Post follow and updates notifications V1 (#111)
* It is now possible to follow a post in order to receive updates about it * Notifications are now sent when updates are published * Post status changes are now tracked * Update sidebar now shows the post status history * Mark a comment as a post update using the comment form * ... more ...
This commit is contained in:
committed by
GitHub
parent
ce7be1b30c
commit
dad382d2b1
@@ -68,8 +68,30 @@ class CommentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_notifications(comment)
|
def send_notifications(comment)
|
||||||
if comment.post.user.notifications_enabled?
|
if comment.is_post_update # Post update
|
||||||
UserMailer.notify_post_owner(comment: comment).deliver_later
|
UserMailer.notify_followers_of_post_update(comment: comment).deliver_later
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if comment.parent_id == nil # Reply to a post
|
||||||
|
user = comment.post.user
|
||||||
|
|
||||||
|
if comment.user.id != user.id and
|
||||||
|
user.notifications_enabled? and
|
||||||
|
comment.post.follows.exists?(user_id: user.id)
|
||||||
|
|
||||||
|
UserMailer.notify_post_owner(comment: comment).deliver_later
|
||||||
|
end
|
||||||
|
else # Reply to a comment
|
||||||
|
parent_comment = comment.parent
|
||||||
|
user = parent_comment.user
|
||||||
|
|
||||||
|
if user.notifications_enabled? and
|
||||||
|
parent_comment.user.id != comment.user.id
|
||||||
|
|
||||||
|
UserMailer.notify_comment_owner(comment: comment).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
53
app/controllers/follows_controller.rb
Normal file
53
app/controllers/follows_controller.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
class FollowsController < ApplicationController
|
||||||
|
before_action :authenticate_user!, only: [:create, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
unless user_signed_in?
|
||||||
|
render json: { }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
follow = Follow.find_by(follow_params)
|
||||||
|
render json: follow
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
follow = Follow.new(follow_params)
|
||||||
|
|
||||||
|
if follow.save
|
||||||
|
render json: {
|
||||||
|
id: follow.id
|
||||||
|
}, status: :created
|
||||||
|
else
|
||||||
|
render json: {
|
||||||
|
error: I18n.t('errors.follows.create', message: follow.errors.full_messages)
|
||||||
|
}, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
follow = Follow.find_by(follow_params)
|
||||||
|
id = follow.id
|
||||||
|
|
||||||
|
return if follow.nil?
|
||||||
|
|
||||||
|
if follow.destroy
|
||||||
|
render json: {
|
||||||
|
id: id,
|
||||||
|
}, status: :accepted
|
||||||
|
else
|
||||||
|
render json: {
|
||||||
|
error: I18n.t('errors.follow.destroy', message: follow.errors.full_messages)
|
||||||
|
}, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def follow_params
|
||||||
|
{
|
||||||
|
post_id: params[:post_id],
|
||||||
|
user_id: current_user.id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
16
app/controllers/post_status_changes_controller.rb
Normal file
16
app/controllers/post_status_changes_controller.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class PostStatusChangesController < ApplicationController
|
||||||
|
def index
|
||||||
|
post_status_changes = PostStatusChange
|
||||||
|
.select(
|
||||||
|
:post_status_id,
|
||||||
|
:updated_at,
|
||||||
|
'users.full_name as user_full_name',
|
||||||
|
'users.email as user_email',
|
||||||
|
)
|
||||||
|
.where(post_id: params[:post_id])
|
||||||
|
.left_outer_joins(:user)
|
||||||
|
.order(updated_at: :asc)
|
||||||
|
|
||||||
|
render json: post_status_changes
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,6 +28,8 @@ class PostsController < ApplicationController
|
|||||||
post = Post.new(post_params)
|
post = Post.new(post_params)
|
||||||
|
|
||||||
if post.save
|
if post.save
|
||||||
|
Follow.create(post_id: post.id, user_id: current_user.id)
|
||||||
|
|
||||||
render json: post, status: :created
|
render json: post, status: :created
|
||||||
else
|
else
|
||||||
render json: {
|
render json: {
|
||||||
@@ -57,9 +59,27 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
post.board_id = params[:post][:board_id] if params[:post].has_key?(:board_id)
|
post.board_id = params[:post][:board_id] if params[:post].has_key?(:board_id)
|
||||||
post.post_status_id = params[:post][:post_status_id] if params[:post].has_key?(:post_status_id)
|
|
||||||
|
post_status_changed = false
|
||||||
|
|
||||||
|
if params[:post].has_key?(:post_status_id) and
|
||||||
|
params[:post][:post_status_id] != post.post_status_id
|
||||||
|
|
||||||
|
post_status_changed = true
|
||||||
|
post.post_status_id = params[:post][:post_status_id]
|
||||||
|
end
|
||||||
|
|
||||||
if post.save
|
if post.save
|
||||||
|
if post_status_changed
|
||||||
|
PostStatusChange.create(
|
||||||
|
user_id: current_user.id,
|
||||||
|
post_id: post.id,
|
||||||
|
post_status_id: post.post_status_id
|
||||||
|
)
|
||||||
|
|
||||||
|
send_notifications(post)
|
||||||
|
end
|
||||||
|
|
||||||
render json: post, status: :no_content
|
render json: post, status: :no_content
|
||||||
else
|
else
|
||||||
render json: {
|
render json: {
|
||||||
@@ -85,4 +105,8 @@ class PostsController < ApplicationController
|
|||||||
.permit(:title, :description, :board_id)
|
.permit(:title, :description, :board_id)
|
||||||
.merge(user_id: current_user.id)
|
.merge(user_id: current_user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_notifications(post)
|
||||||
|
UserMailer.notify_followers_of_post_status_change(post: post).deliver_later
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ interface SetCommentReplyBodyAction {
|
|||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TOGGLE_COMMENT_IS_POST_UPDATE_FLAG = 'TOGGLE_COMMENT_IS_POST_UPDATE_FLAG';
|
||||||
|
interface ToggleCommentIsPostUpdateFlag {
|
||||||
|
type: typeof TOGGLE_COMMENT_IS_POST_UPDATE_FLAG;
|
||||||
|
commentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const toggleCommentReply = (commentId: number): ToggleCommentReplyAction => ({
|
export const toggleCommentReply = (commentId: number): ToggleCommentReplyAction => ({
|
||||||
type: TOGGLE_COMMENT_REPLY,
|
type: TOGGLE_COMMENT_REPLY,
|
||||||
commentId,
|
commentId,
|
||||||
@@ -22,6 +28,12 @@ export const setCommentReplyBody = (commentId: number, body: string): SetComment
|
|||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const toggleCommentIsPostUpdateFlag = (commentId: number): ToggleCommentIsPostUpdateFlag => ({
|
||||||
|
type: TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||||
|
commentId,
|
||||||
|
});
|
||||||
|
|
||||||
export type HandleCommentRepliesType =
|
export type HandleCommentRepliesType =
|
||||||
ToggleCommentReplyAction |
|
ToggleCommentReplyAction |
|
||||||
SetCommentReplyBodyAction;
|
SetCommentReplyBodyAction |
|
||||||
|
ToggleCommentIsPostUpdateFlag;
|
||||||
@@ -52,6 +52,7 @@ export const submitComment = (
|
|||||||
postId: number,
|
postId: number,
|
||||||
body: string,
|
body: string,
|
||||||
parentId: number,
|
parentId: number,
|
||||||
|
isPostUpdate: boolean,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
dispatch(commentSubmitStart(parentId));
|
dispatch(commentSubmitStart(parentId));
|
||||||
@@ -64,6 +65,7 @@ export const submitComment = (
|
|||||||
comment: {
|
comment: {
|
||||||
body,
|
body,
|
||||||
parent_id: parentId,
|
parent_id: parentId,
|
||||||
|
is_post_update: isPostUpdate,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
58
app/javascript/actions/Follow/requestFollow.ts
Normal file
58
app/javascript/actions/Follow/requestFollow.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Action } from 'redux';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import IFollowJSON from '../../interfaces/json/IFollow';
|
||||||
|
|
||||||
|
import { State } from '../../reducers/rootReducer';
|
||||||
|
|
||||||
|
export const FOLLOW_REQUEST_START = 'FOLLOW_REQUEST_START';
|
||||||
|
interface FollowRequestStartAction {
|
||||||
|
type: typeof FOLLOW_REQUEST_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FOLLOW_REQUEST_SUCCESS = 'FOLLOW_REQUEST_SUCCESS';
|
||||||
|
interface FollowRequestSuccessAction {
|
||||||
|
type: typeof FOLLOW_REQUEST_SUCCESS;
|
||||||
|
follow: IFollowJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FOLLOW_REQUEST_FAILURE = 'FOLLOW_REQUEST_FAILURE';
|
||||||
|
interface FollowRequestFailureAction {
|
||||||
|
type: typeof FOLLOW_REQUEST_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FollowRequestActionTypes =
|
||||||
|
FollowRequestStartAction |
|
||||||
|
FollowRequestSuccessAction |
|
||||||
|
FollowRequestFailureAction;
|
||||||
|
|
||||||
|
const followRequestStart = (): FollowRequestActionTypes => ({
|
||||||
|
type: FOLLOW_REQUEST_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const followRequestSuccess = (
|
||||||
|
follow: IFollowJSON,
|
||||||
|
): FollowRequestActionTypes => ({
|
||||||
|
type: FOLLOW_REQUEST_SUCCESS,
|
||||||
|
follow,
|
||||||
|
});
|
||||||
|
|
||||||
|
const followRequestFailure = (error: string): FollowRequestActionTypes => ({
|
||||||
|
type: FOLLOW_REQUEST_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestFollow = (
|
||||||
|
postId: number,
|
||||||
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
dispatch(followRequestStart());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/posts/${postId}/follows`);
|
||||||
|
const json = await response.json();
|
||||||
|
dispatch(followRequestSuccess(json));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(followRequestFailure(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
47
app/javascript/actions/Follow/submitFollow.ts
Normal file
47
app/javascript/actions/Follow/submitFollow.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Action } from "redux";
|
||||||
|
import { ThunkAction } from "redux-thunk";
|
||||||
|
|
||||||
|
import { State } from "../../reducers/rootReducer";
|
||||||
|
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
|
||||||
|
import HttpStatus from "../../constants/http_status";
|
||||||
|
import IFollowJSON from "../../interfaces/json/IFollow";
|
||||||
|
|
||||||
|
export const FOLLOW_SUBMIT_SUCCESS = 'FOLLOW_SUBMIT_SUCCESS';
|
||||||
|
interface FollowSubmitSuccessAction {
|
||||||
|
type: typeof FOLLOW_SUBMIT_SUCCESS,
|
||||||
|
postId: number;
|
||||||
|
isFollow: boolean;
|
||||||
|
follow: IFollowJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FollowActionTypes = FollowSubmitSuccessAction;
|
||||||
|
|
||||||
|
const followSubmitSuccess = (
|
||||||
|
postId: number,
|
||||||
|
isFollow: boolean,
|
||||||
|
follow: IFollowJSON,
|
||||||
|
): FollowSubmitSuccessAction => ({
|
||||||
|
type: FOLLOW_SUBMIT_SUCCESS,
|
||||||
|
postId,
|
||||||
|
isFollow,
|
||||||
|
follow,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const submitFollow = (
|
||||||
|
postId: number,
|
||||||
|
isFollow: boolean,
|
||||||
|
authenticityToken: string,
|
||||||
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/posts/${postId}/follows`, {
|
||||||
|
method: isFollow ? 'POST' : 'DELETE',
|
||||||
|
headers: buildRequestHeaders(authenticityToken),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (res.status === HttpStatus.Created || res.status === HttpStatus.Accepted)
|
||||||
|
dispatch(followSubmitSuccess(postId, isFollow, json));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('An error occurred while following a post');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Action } from 'redux';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import IPostStatusChangeJSON from '../../interfaces/json/IPostStatusChange';
|
||||||
|
|
||||||
|
import { State } from '../../reducers/rootReducer';
|
||||||
|
|
||||||
|
export const POST_STATUS_CHANGES_REQUEST_START = 'POST_STATUS_CHANGES_REQUEST_START';
|
||||||
|
interface PostStatusChangesRequestStartAction {
|
||||||
|
type: typeof POST_STATUS_CHANGES_REQUEST_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST_STATUS_CHANGES_REQUEST_SUCCESS = 'POST_STATUS_CHANGES_REQUEST_SUCCESS';
|
||||||
|
interface PostStatusChangesRequestSuccessAction {
|
||||||
|
type: typeof POST_STATUS_CHANGES_REQUEST_SUCCESS;
|
||||||
|
postStatusChanges: Array<IPostStatusChangeJSON>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST_STATUS_CHANGES_REQUEST_FAILURE = 'POST_STATUS_CHANGES_REQUEST_FAILURE';
|
||||||
|
interface PostStatusChangesRequestFailureAction {
|
||||||
|
type: typeof POST_STATUS_CHANGES_REQUEST_FAILURE;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostStatusChangesRequestActionTypes =
|
||||||
|
PostStatusChangesRequestStartAction |
|
||||||
|
PostStatusChangesRequestSuccessAction |
|
||||||
|
PostStatusChangesRequestFailureAction;
|
||||||
|
|
||||||
|
const postStatusChangesRequestStart = (): PostStatusChangesRequestActionTypes => ({
|
||||||
|
type: POST_STATUS_CHANGES_REQUEST_START,
|
||||||
|
});
|
||||||
|
|
||||||
|
const postStatusChangesRequestSuccess = (
|
||||||
|
postStatusChanges: Array<IPostStatusChangeJSON>,
|
||||||
|
): PostStatusChangesRequestActionTypes => ({
|
||||||
|
type: POST_STATUS_CHANGES_REQUEST_SUCCESS,
|
||||||
|
postStatusChanges,
|
||||||
|
});
|
||||||
|
|
||||||
|
const postStatusChangesRequestFailure = (error: string): PostStatusChangesRequestActionTypes => ({
|
||||||
|
type: POST_STATUS_CHANGES_REQUEST_FAILURE,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const requestPostStatusChanges = (
|
||||||
|
postId: number,
|
||||||
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
|
dispatch(postStatusChangesRequestStart());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/posts/${postId}/post_status_changes`);
|
||||||
|
const json = await response.json();
|
||||||
|
dispatch(postStatusChangesRequestSuccess(json));
|
||||||
|
} catch (e) {
|
||||||
|
dispatch(postStatusChangesRequestFailure(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import IPostStatusChange from "../../interfaces/IPostStatusChange";
|
||||||
|
|
||||||
|
export const POST_STATUS_CHANGE_SUBMITTED = 'POST_STATUS_CHANGE_SUBMITTED';
|
||||||
|
export interface PostStatusChangeSubmitted {
|
||||||
|
type: typeof POST_STATUS_CHANGE_SUBMITTED;
|
||||||
|
postStatusChange: IPostStatusChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const postStatusChangeSubmitted = (
|
||||||
|
postStatusChange: IPostStatusChange
|
||||||
|
): PostStatusChangeSubmitted => ({
|
||||||
|
type: POST_STATUS_CHANGE_SUBMITTED,
|
||||||
|
postStatusChange,
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import { MutedText } from '../shared/CustomTexts';
|
|||||||
|
|
||||||
import { ReplyFormState } from '../../reducers/replyFormReducer';
|
import { ReplyFormState } from '../../reducers/replyFormReducer';
|
||||||
|
|
||||||
import friendlyDate from '../../helpers/friendlyDate';
|
import friendlyDate from '../../helpers/datetime';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -21,7 +21,7 @@ interface Props {
|
|||||||
handleToggleCommentReply(): void;
|
handleToggleCommentReply(): void;
|
||||||
handleCommentReplyBodyChange(e: React.FormEvent): void;
|
handleCommentReplyBodyChange(e: React.FormEvent): void;
|
||||||
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
||||||
handleSubmitComment(body: string, parentId: number): void;
|
handleSubmitComment(body: string, parentId: number, isPostUpdate: boolean): void;
|
||||||
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isPowerUser: boolean;
|
isPowerUser: boolean;
|
||||||
@@ -48,7 +48,7 @@ const Comment = ({
|
|||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div className="comment">
|
<div className="comment">
|
||||||
<div className="commentHeader">
|
<div className="commentHeader">
|
||||||
<Gravatar email={userEmail} size={24} className="gravatar" />
|
<Gravatar email={userEmail} size={28} className="gravatar" />
|
||||||
<span className="commentAuthor">{userFullName}</span>
|
<span className="commentAuthor">{userFullName}</span>
|
||||||
{ isPostUpdate ? <span className="postUpdateBadge">Post update</span> : null }
|
{ isPostUpdate ? <span className="postUpdateBadge">Post update</span> : null }
|
||||||
</div>
|
</div>
|
||||||
@@ -89,12 +89,15 @@ const Comment = ({
|
|||||||
<NewComment
|
<NewComment
|
||||||
body={replyForm.body}
|
body={replyForm.body}
|
||||||
parentId={id}
|
parentId={id}
|
||||||
|
postUpdateFlagValue={replyForm.isPostUpdate}
|
||||||
isSubmitting={replyForm.isSubmitting}
|
isSubmitting={replyForm.isSubmitting}
|
||||||
error={replyForm.error}
|
error={replyForm.error}
|
||||||
handleChange={handleCommentReplyBodyChange}
|
handleChange={handleCommentReplyBodyChange}
|
||||||
|
handlePostUpdateFlag={() => null}
|
||||||
handleSubmit={handleSubmitComment}
|
handleSubmit={handleSubmitComment}
|
||||||
|
|
||||||
isLoggedIn={isLoggedIn}
|
isLoggedIn={isLoggedIn}
|
||||||
|
isPowerUser={isPowerUser}
|
||||||
userEmail={currentUserEmail}
|
userEmail={currentUserEmail}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface Props {
|
|||||||
toggleCommentReply(commentId: number): void;
|
toggleCommentReply(commentId: number): void;
|
||||||
setCommentReplyBody(commentId: number, body: string): void;
|
setCommentReplyBody(commentId: number, body: string): void;
|
||||||
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
||||||
handleSubmitComment(body: string, parentId: number): void;
|
handleSubmitComment(body: string, parentId: number, isPostUpdate: boolean): void;
|
||||||
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isPowerUser: boolean;
|
isPowerUser: boolean;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface Props {
|
|||||||
requestComments(postId: number, page?: number): void;
|
requestComments(postId: number, page?: number): void;
|
||||||
toggleCommentReply(commentId: number): void;
|
toggleCommentReply(commentId: number): void;
|
||||||
setCommentReplyBody(commentId: number, body: string): void;
|
setCommentReplyBody(commentId: number, body: string): void;
|
||||||
|
toggleCommentIsPostUpdateFlag(): void;
|
||||||
toggleCommentIsPostUpdate(
|
toggleCommentIsPostUpdate(
|
||||||
postId: number,
|
postId: number,
|
||||||
commentId: number,
|
commentId: number,
|
||||||
@@ -33,6 +34,7 @@ interface Props {
|
|||||||
postId: number,
|
postId: number,
|
||||||
body: string,
|
body: string,
|
||||||
parentId: number,
|
parentId: number,
|
||||||
|
isPostUpdate: boolean,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
@@ -51,11 +53,12 @@ class CommentsP extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleSubmitComment = (body: string, parentId: number) => {
|
_handleSubmitComment = (body: string, parentId: number, isPostUpdate: boolean) => {
|
||||||
this.props.submitComment(
|
this.props.submitComment(
|
||||||
this.props.postId,
|
this.props.postId,
|
||||||
body,
|
body,
|
||||||
parentId,
|
parentId,
|
||||||
|
isPostUpdate,
|
||||||
this.props.authenticityToken,
|
this.props.authenticityToken,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,6 +76,7 @@ class CommentsP extends React.Component<Props> {
|
|||||||
|
|
||||||
toggleCommentReply,
|
toggleCommentReply,
|
||||||
setCommentReplyBody,
|
setCommentReplyBody,
|
||||||
|
toggleCommentIsPostUpdateFlag,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const postReply = replyForms.find(replyForm => replyForm.commentId === null);
|
const postReply = replyForms.find(replyForm => replyForm.commentId === null);
|
||||||
@@ -82,6 +86,7 @@ class CommentsP extends React.Component<Props> {
|
|||||||
<NewComment
|
<NewComment
|
||||||
body={postReply && postReply.body}
|
body={postReply && postReply.body}
|
||||||
parentId={null}
|
parentId={null}
|
||||||
|
postUpdateFlagValue={postReply && postReply.isPostUpdate}
|
||||||
isSubmitting={postReply && postReply.isSubmitting}
|
isSubmitting={postReply && postReply.isSubmitting}
|
||||||
error={postReply && postReply.error}
|
error={postReply && postReply.error}
|
||||||
handleChange={
|
handleChange={
|
||||||
@@ -89,9 +94,11 @@ class CommentsP extends React.Component<Props> {
|
|||||||
setCommentReplyBody(null, (e.target as HTMLTextAreaElement).value)
|
setCommentReplyBody(null, (e.target as HTMLTextAreaElement).value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
handlePostUpdateFlag={toggleCommentIsPostUpdateFlag}
|
||||||
handleSubmit={this._handleSubmitComment}
|
handleSubmit={this._handleSubmitComment}
|
||||||
|
|
||||||
isLoggedIn={isLoggedIn}
|
isLoggedIn={isLoggedIn}
|
||||||
|
isPowerUser={isPowerUser}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -99,7 +106,7 @@ class CommentsP extends React.Component<Props> {
|
|||||||
{ error ? <DangerText>{error}</DangerText> : null }
|
{ error ? <DangerText>{error}</DangerText> : null }
|
||||||
|
|
||||||
<div className="commentsTitle">
|
<div className="commentsTitle">
|
||||||
activity • {comments.length} comments
|
activity • {comments.length} comment{comments.length === 1 ? '' : 's'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommentList
|
<CommentList
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Gravatar from 'react-gravatar';
|
import Gravatar from 'react-gravatar';
|
||||||
|
|
||||||
|
import NewCommentUpdateSection from './NewCommentUpdateSection';
|
||||||
|
|
||||||
import Button from '../shared/Button';
|
import Button from '../shared/Button';
|
||||||
import Spinner from '../shared/Spinner';
|
import Spinner from '../shared/Spinner';
|
||||||
import { DangerText } from '../shared/CustomTexts';
|
import { DangerText } from '../shared/CustomTexts';
|
||||||
@@ -8,24 +10,34 @@ import { DangerText } from '../shared/CustomTexts';
|
|||||||
interface Props {
|
interface Props {
|
||||||
body: string;
|
body: string;
|
||||||
parentId: number;
|
parentId: number;
|
||||||
|
postUpdateFlagValue: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
handleChange(e: React.FormEvent): void;
|
handleChange(e: React.FormEvent): void;
|
||||||
handleSubmit(body: string, parentId: number): void;
|
handlePostUpdateFlag(): void;
|
||||||
|
handleSubmit(
|
||||||
|
body: string,
|
||||||
|
parentId: number,
|
||||||
|
isPostUpdate: boolean
|
||||||
|
): void;
|
||||||
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
isPowerUser: boolean;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewComment = ({
|
const NewComment = ({
|
||||||
body,
|
body,
|
||||||
parentId,
|
parentId,
|
||||||
|
postUpdateFlagValue,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
error,
|
error,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
handlePostUpdateFlag,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isPowerUser,
|
||||||
userEmail,
|
userEmail,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -33,18 +45,29 @@ const NewComment = ({
|
|||||||
{
|
{
|
||||||
isLoggedIn ?
|
isLoggedIn ?
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Gravatar email={userEmail} size={36} className="currentUserAvatar" />
|
<div className="commentBodyForm">
|
||||||
<textarea
|
<Gravatar email={userEmail} size={48} className="currentUserAvatar" />
|
||||||
value={body}
|
<textarea
|
||||||
onChange={handleChange}
|
value={body}
|
||||||
placeholder="Leave a comment"
|
onChange={handleChange}
|
||||||
className="newCommentBody"
|
placeholder="Leave a comment"
|
||||||
/>
|
className="newCommentBody"
|
||||||
<Button
|
/>
|
||||||
onClick={() => handleSubmit(body, parentId)}
|
<Button
|
||||||
className="submitCommentButton">
|
onClick={() => handleSubmit(body, parentId, postUpdateFlagValue)}
|
||||||
{ isSubmitting ? <Spinner color="light" /> : 'Submit' }
|
className="submitCommentButton">
|
||||||
</Button>
|
{ isSubmitting ? <Spinner color="light" /> : 'Submit' }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
isPowerUser && parentId == null ?
|
||||||
|
<NewCommentUpdateSection
|
||||||
|
postUpdateFlagValue={postUpdateFlagValue}
|
||||||
|
handlePostUpdateFlag={handlePostUpdateFlag}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
:
|
:
|
||||||
<a href="/users/sign_in" className="loginInfo">You need to log in to post comments.</a>
|
<a href="/users/sign_in" className="loginInfo">You need to log in to post comments.</a>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { MutedText } from '../shared/CustomTexts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
postUpdateFlagValue: boolean;
|
||||||
|
handlePostUpdateFlag(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NewCommentUpdateSection = ({
|
||||||
|
postUpdateFlagValue,
|
||||||
|
handlePostUpdateFlag,
|
||||||
|
}: Props) => (
|
||||||
|
<div className="commentIsUpdateForm">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="isPostUpdateFlag"
|
||||||
|
type="checkbox"
|
||||||
|
onChange={handlePostUpdateFlag}
|
||||||
|
checked={postUpdateFlagValue || false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor="isPostUpdateFlag">Mark as post update</label>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
postUpdateFlagValue ?
|
||||||
|
<MutedText>Users that follow this post will be notified</MutedText>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NewCommentUpdateSection;
|
||||||
33
app/javascript/components/Post/ActionBox.tsx
Normal file
33
app/javascript/components/Post/ActionBox.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Button from '../shared/Button';
|
||||||
|
|
||||||
|
import { BoxTitleText, SmallMutedText } from '../shared/CustomTexts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
followed: boolean;
|
||||||
|
submitFollow(): void;
|
||||||
|
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionBox = ({followed, submitFollow, isLoggedIn}: Props) => (
|
||||||
|
<div className="actionBoxContainer">
|
||||||
|
<div className="actionBoxFollow">
|
||||||
|
<BoxTitleText>Actions</BoxTitleText>
|
||||||
|
<br />
|
||||||
|
<Button onClick={isLoggedIn ? submitFollow : () => location.href = '/users/sign_in'} outline>
|
||||||
|
{ followed ? 'Unfollow post' : 'Follow post' }
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<SmallMutedText>
|
||||||
|
{ followed ?
|
||||||
|
'you\'re receiving notifications about new updates on this post'
|
||||||
|
:
|
||||||
|
'you won\'t receive notifications about this post'
|
||||||
|
}
|
||||||
|
</SmallMutedText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ActionBox;
|
||||||
@@ -4,7 +4,9 @@ import IPost from '../../interfaces/IPost';
|
|||||||
import IPostStatus from '../../interfaces/IPostStatus';
|
import IPostStatus from '../../interfaces/IPostStatus';
|
||||||
import IBoard from '../../interfaces/IBoard';
|
import IBoard from '../../interfaces/IBoard';
|
||||||
|
|
||||||
|
import PostUpdateList from './PostUpdateList';
|
||||||
import LikeList from './LikeList';
|
import LikeList from './LikeList';
|
||||||
|
import ActionBox from './ActionBox';
|
||||||
import LikeButton from '../../containers/LikeButton';
|
import LikeButton from '../../containers/LikeButton';
|
||||||
import PostBoardSelect from './PostBoardSelect';
|
import PostBoardSelect from './PostBoardSelect';
|
||||||
import PostStatusSelect from './PostStatusSelect';
|
import PostStatusSelect from './PostStatusSelect';
|
||||||
@@ -13,25 +15,31 @@ import PostStatusLabel from '../shared/PostStatusLabel';
|
|||||||
import Comments from '../../containers/Comments';
|
import Comments from '../../containers/Comments';
|
||||||
import { MutedText } from '../shared/CustomTexts';
|
import { MutedText } from '../shared/CustomTexts';
|
||||||
|
|
||||||
import friendlyDate from '../../helpers/friendlyDate';
|
|
||||||
import { LikesState } from '../../reducers/likesReducer';
|
import { LikesState } from '../../reducers/likesReducer';
|
||||||
import { CommentsState } from '../../reducers/commentsReducer';
|
import { CommentsState } from '../../reducers/commentsReducer';
|
||||||
import PostUpdateList from './PostUpdateList';
|
import { PostStatusChangesState } from '../../reducers/postStatusChangesReducer';
|
||||||
|
|
||||||
|
import friendlyDate, { fromRailsStringToJavascriptDate } from '../../helpers/datetime';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postId: number;
|
postId: number;
|
||||||
post: IPost;
|
post: IPost;
|
||||||
likes: LikesState;
|
likes: LikesState;
|
||||||
|
followed: boolean;
|
||||||
comments: CommentsState;
|
comments: CommentsState;
|
||||||
|
postStatusChanges: PostStatusChangesState;
|
||||||
boards: Array<IBoard>;
|
boards: Array<IBoard>;
|
||||||
postStatuses: Array<IPostStatus>;
|
postStatuses: Array<IPostStatus>;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isPowerUser: boolean;
|
isPowerUser: boolean;
|
||||||
|
userFullName: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
|
|
||||||
requestPost(postId: number): void;
|
requestPost(postId: number): void;
|
||||||
requestLikes(postId: number): void;
|
requestLikes(postId: number): void;
|
||||||
|
requestFollow(postId: number): void;
|
||||||
|
requestPostStatusChanges(postId: number): void;
|
||||||
changePostBoard(
|
changePostBoard(
|
||||||
postId: number,
|
postId: number,
|
||||||
newBoardId: number,
|
newBoardId: number,
|
||||||
@@ -40,38 +48,62 @@ interface Props {
|
|||||||
changePostStatus(
|
changePostStatus(
|
||||||
postId: number,
|
postId: number,
|
||||||
newPostStatusId: number,
|
newPostStatusId: number,
|
||||||
|
userFullName: string,
|
||||||
|
userEmail: string,
|
||||||
|
authenticityToken: string,
|
||||||
|
): void;
|
||||||
|
submitFollow(
|
||||||
|
postId: number,
|
||||||
|
isFollow: boolean,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostP extends React.Component<Props> {
|
class PostP extends React.Component<Props> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.requestPost(this.props.postId);
|
const {postId} = this.props;
|
||||||
this.props.requestLikes(this.props.postId);
|
|
||||||
|
this.props.requestPost(postId);
|
||||||
|
this.props.requestLikes(postId);
|
||||||
|
this.props.requestFollow(postId);
|
||||||
|
this.props.requestPostStatusChanges(postId);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
post,
|
post,
|
||||||
likes,
|
likes,
|
||||||
|
followed,
|
||||||
comments,
|
comments,
|
||||||
|
postStatusChanges,
|
||||||
boards,
|
boards,
|
||||||
postStatuses,
|
postStatuses,
|
||||||
|
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
isPowerUser,
|
isPowerUser,
|
||||||
|
userFullName,
|
||||||
userEmail,
|
userEmail,
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
|
|
||||||
changePostBoard,
|
changePostBoard,
|
||||||
changePostStatus,
|
changePostStatus,
|
||||||
|
submitFollow,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const postUpdates = [
|
||||||
|
...comments.items.filter(comment => comment.isPostUpdate === true),
|
||||||
|
...postStatusChanges.items,
|
||||||
|
].sort(
|
||||||
|
(a, b) =>
|
||||||
|
fromRailsStringToJavascriptDate(a.updatedAt) < fromRailsStringToJavascriptDate(b.updatedAt) ? 1 : -1
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pageContainer">
|
<div className="pageContainer">
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<PostUpdateList
|
<PostUpdateList
|
||||||
postUpdates={comments.items.filter(comment => comment.isPostUpdate === true)}
|
postUpdates={postUpdates}
|
||||||
|
postStatuses={postStatuses}
|
||||||
areLoading={comments.areLoading}
|
areLoading={comments.areLoading}
|
||||||
error={comments.error}
|
error={comments.error}
|
||||||
/>
|
/>
|
||||||
@@ -81,6 +113,13 @@ class PostP extends React.Component<Props> {
|
|||||||
areLoading={likes.areLoading}
|
areLoading={likes.areLoading}
|
||||||
error={likes.error}
|
error={likes.error}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ActionBox
|
||||||
|
followed={followed}
|
||||||
|
submitFollow={() => submitFollow(post.id, !followed, authenticityToken)}
|
||||||
|
|
||||||
|
isLoggedIn={isLoggedIn}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="postAndCommentsContainer">
|
<div className="postAndCommentsContainer">
|
||||||
@@ -113,7 +152,8 @@ class PostP extends React.Component<Props> {
|
|||||||
postStatuses={postStatuses}
|
postStatuses={postStatuses}
|
||||||
selectedPostStatusId={post.postStatusId}
|
selectedPostStatusId={post.postStatusId}
|
||||||
handleChange={
|
handleChange={
|
||||||
newPostStatusId => changePostStatus(post.id, newPostStatusId, authenticityToken)
|
newPostStatusId =>
|
||||||
|
changePostStatus(post.id, newPostStatusId, userFullName, userEmail, authenticityToken)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ import { BoxTitleText, DangerText, CenteredMutedText, MutedText } from '../share
|
|||||||
import Spinner from '../shared/Spinner';
|
import Spinner from '../shared/Spinner';
|
||||||
|
|
||||||
import IComment from '../../interfaces/IComment';
|
import IComment from '../../interfaces/IComment';
|
||||||
|
import IPostStatusChange from '../../interfaces/IPostStatusChange';
|
||||||
|
import IPostStatus from '../../interfaces/IPostStatus';
|
||||||
|
|
||||||
import friendlyDate from '../../helpers/friendlyDate';
|
import friendlyDate from '../../helpers/datetime';
|
||||||
|
import PostStatusLabel from '../shared/PostStatusLabel';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postUpdates: Array<IComment>;
|
postUpdates: Array<IComment | IPostStatusChange>;
|
||||||
|
postStatuses: Array<IPostStatus>
|
||||||
areLoading: boolean;
|
areLoading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostUpdateList = ({
|
const PostUpdateList = ({
|
||||||
postUpdates,
|
postUpdates,
|
||||||
|
postStatuses,
|
||||||
areLoading,
|
areLoading,
|
||||||
error,
|
error,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
@@ -33,7 +38,18 @@ const PostUpdateList = ({
|
|||||||
<span>{postUpdate.userFullName}</span>
|
<span>{postUpdate.userFullName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="postUpdateListItemBody">{postUpdate.body}</p>
|
<p className="postUpdateListItemBody">
|
||||||
|
{ 'body' in postUpdate ?
|
||||||
|
postUpdate.body
|
||||||
|
:
|
||||||
|
<React.Fragment>
|
||||||
|
<i>changed status to</i>
|
||||||
|
<PostStatusLabel
|
||||||
|
{...postStatuses.find(postStatus => postStatus.id === postUpdate.postStatusId)}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
<MutedText>{friendlyDate(postUpdate.updatedAt)}</MutedText>
|
<MutedText>{friendlyDate(postUpdate.updatedAt)}</MutedText>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
postStatuses: Array<IPostStatus>;
|
postStatuses: Array<IPostStatus>;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
isPowerUser: boolean;
|
isPowerUser: boolean;
|
||||||
|
userFullName: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
authenticityToken: string;
|
authenticityToken: string;
|
||||||
}
|
}
|
||||||
@@ -37,6 +38,7 @@ class PostRoot extends React.Component<Props> {
|
|||||||
postStatuses,
|
postStatuses,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
isPowerUser,
|
isPowerUser,
|
||||||
|
userFullName,
|
||||||
userEmail,
|
userEmail,
|
||||||
authenticityToken
|
authenticityToken
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -50,6 +52,7 @@ class PostRoot extends React.Component<Props> {
|
|||||||
|
|
||||||
isLoggedIn={isLoggedIn}
|
isLoggedIn={isLoggedIn}
|
||||||
isPowerUser={isPowerUser}
|
isPowerUser={isPowerUser}
|
||||||
|
userFullName={userFullName}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
authenticityToken={authenticityToken}
|
authenticityToken={authenticityToken}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export const CenteredMutedText = ({ children }: Props) => (
|
|||||||
<p className="centeredText"><span className="mutedText">{children}</span></p>
|
<p className="centeredText"><span className="mutedText">{children}</span></p>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SmallMutedText = ({ children }: Props) => (
|
||||||
|
<p className="smallMutedText">{children}</p>
|
||||||
|
);
|
||||||
|
|
||||||
export const UppercaseText = ({ children }: Props) => (
|
export const UppercaseText = ({ children }: Props) => (
|
||||||
<span className="uppercaseText">{children}</span>
|
<span className="uppercaseText">{children}</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const PostStatusLabel = ({
|
|||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<span className="badge" style={{backgroundColor: color, color: 'white'}}>
|
<span className="badge" style={{backgroundColor: color || 'black', color: 'white'}}>
|
||||||
{name?.toUpperCase()}
|
{(name || 'no status').toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { requestComments } from '../actions/Comment/requestComments';
|
|||||||
import {
|
import {
|
||||||
toggleCommentReply,
|
toggleCommentReply,
|
||||||
setCommentReplyBody,
|
setCommentReplyBody,
|
||||||
|
toggleCommentIsPostUpdateFlag,
|
||||||
} from '../actions/Comment/handleCommentReplies';
|
} from '../actions/Comment/handleCommentReplies';
|
||||||
import { toggleCommentIsUpdate } from '../actions/Comment/updateComment';
|
import { toggleCommentIsUpdate } from '../actions/Comment/updateComment';
|
||||||
import { submitComment } from '../actions/Comment/submitComment';
|
import { submitComment } from '../actions/Comment/submitComment';
|
||||||
@@ -32,6 +33,10 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setCommentReplyBody(commentId, body));
|
dispatch(setCommentReplyBody(commentId, body));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleCommentIsPostUpdateFlag() {
|
||||||
|
dispatch(toggleCommentIsPostUpdateFlag(null));
|
||||||
|
},
|
||||||
|
|
||||||
toggleCommentIsPostUpdate(
|
toggleCommentIsPostUpdate(
|
||||||
postId: number,
|
postId: number,
|
||||||
commentId: number,
|
commentId: number,
|
||||||
@@ -45,9 +50,10 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
postId: number,
|
postId: number,
|
||||||
body: string,
|
body: string,
|
||||||
parentId: number,
|
parentId: number,
|
||||||
|
isPostUpdate: boolean,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
) {
|
) {
|
||||||
dispatch(submitComment(postId, body, parentId, authenticityToken));
|
dispatch(submitComment(postId, body, parentId, isPostUpdate, authenticityToken));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import { requestPost } from '../actions/Post/requestPost';
|
|||||||
import { requestLikes } from '../actions/Like/requestLikes';
|
import { requestLikes } from '../actions/Like/requestLikes';
|
||||||
import { changePostBoard } from '../actions/Post/changePostBoard';
|
import { changePostBoard } from '../actions/Post/changePostBoard';
|
||||||
import { changePostStatus } from '../actions/Post/changePostStatus';
|
import { changePostStatus } from '../actions/Post/changePostStatus';
|
||||||
|
import { submitFollow } from '../actions/Follow/submitFollow';
|
||||||
|
import { requestFollow } from '../actions/Follow/requestFollow';
|
||||||
|
import { requestPostStatusChanges } from '../actions/PostStatusChange/requestPostStatusChanges';
|
||||||
|
import { postStatusChangeSubmitted } from '../actions/PostStatusChange/submittedPostStatusChange';
|
||||||
|
|
||||||
import { State } from '../reducers/rootReducer';
|
import { State } from '../reducers/rootReducer';
|
||||||
|
|
||||||
import PostP from '../components/Post/PostP';
|
import PostP from '../components/Post/PostP';
|
||||||
|
|
||||||
|
import { fromJavascriptDateToRailsString } from '../helpers/datetime';
|
||||||
|
|
||||||
const mapStateToProps = (state: State) => ({
|
const mapStateToProps = (state: State) => ({
|
||||||
post: state.currentPost.item,
|
post: state.currentPost.item,
|
||||||
likes: state.currentPost.likes,
|
likes: state.currentPost.likes,
|
||||||
|
followed: state.currentPost.followed,
|
||||||
comments: state.currentPost.comments,
|
comments: state.currentPost.comments,
|
||||||
|
postStatusChanges: state.currentPost.postStatusChanges,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -24,14 +32,41 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(requestLikes(postId));
|
dispatch(requestLikes(postId));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
requestFollow(postId: number) {
|
||||||
|
dispatch(requestFollow(postId));
|
||||||
|
},
|
||||||
|
|
||||||
|
requestPostStatusChanges(postId: number) {
|
||||||
|
dispatch(requestPostStatusChanges(postId));
|
||||||
|
},
|
||||||
|
|
||||||
changePostBoard(postId: number, newBoardId: number, authenticityToken: string) {
|
changePostBoard(postId: number, newBoardId: number, authenticityToken: string) {
|
||||||
dispatch(changePostBoard(postId, newBoardId, authenticityToken));
|
dispatch(changePostBoard(postId, newBoardId, authenticityToken));
|
||||||
},
|
},
|
||||||
|
|
||||||
changePostStatus(postId: number, newPostStatusId: number, authenticityToken: string) {
|
changePostStatus(
|
||||||
|
postId: number,
|
||||||
|
newPostStatusId: number,
|
||||||
|
userFullName: string,
|
||||||
|
userEmail: string,
|
||||||
|
authenticityToken: string
|
||||||
|
) {
|
||||||
if (isNaN(newPostStatusId)) newPostStatusId = null;
|
if (isNaN(newPostStatusId)) newPostStatusId = null;
|
||||||
|
|
||||||
dispatch(changePostStatus(postId, newPostStatusId, authenticityToken));
|
dispatch(changePostStatus(postId, newPostStatusId, authenticityToken)).then(res => {
|
||||||
|
if (res && res.status !== 204) return;
|
||||||
|
|
||||||
|
dispatch(postStatusChangeSubmitted({
|
||||||
|
postStatusId: newPostStatusId,
|
||||||
|
userFullName,
|
||||||
|
userEmail,
|
||||||
|
updatedAt: fromJavascriptDateToRailsString(new Date()),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
submitFollow(postId: number, isFollow: boolean, authenticityToken: string) {
|
||||||
|
dispatch(submitFollow(postId, isFollow, authenticityToken));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const friendlyDate = date => {
|
export const friendlyDate = date => {
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var timeStamp = fromRailsStringToJavascriptDate(date);
|
var timeStamp = fromRailsStringToJavascriptDate(date);
|
||||||
|
|
||||||
@@ -7,13 +7,13 @@ const friendlyDate = date => {
|
|||||||
if (secondsPast < 60) {
|
if (secondsPast < 60) {
|
||||||
return 'just now';
|
return 'just now';
|
||||||
} else if (secondsPast < 3600) {
|
} else if (secondsPast < 3600) {
|
||||||
let minutesPast = parseInt(secondsPast / 60);
|
let minutesPast = Math.round(secondsPast / 60);
|
||||||
return minutesPast + ' ' + (minutesPast === 1 ? 'minute' : 'minutes') + ' ago';
|
return minutesPast + ' ' + (minutesPast === 1 ? 'minute' : 'minutes') + ' ago';
|
||||||
} else if (secondsPast <= 86400) {
|
} else if (secondsPast <= 86400) {
|
||||||
let hoursPast = parseInt(secondsPast / 3600);
|
let hoursPast = Math.round(secondsPast / 3600);
|
||||||
return hoursPast + ' ' + (hoursPast === 1 ? 'hour' : 'hours') + ' ago';
|
return hoursPast + ' ' + (hoursPast === 1 ? 'hour' : 'hours') + ' ago';
|
||||||
} else {
|
} else {
|
||||||
let daysPast = parseInt(secondsPast / 86400);
|
let daysPast = Math.round(secondsPast / 86400);
|
||||||
return daysPast + ' ' + (daysPast === 1 ? 'day' : 'days') + ' ago';
|
return daysPast + ' ' + (daysPast === 1 ? 'day' : 'days') + ' ago';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,13 @@ export default friendlyDate;
|
|||||||
Converts the default Rails datetime string
|
Converts the default Rails datetime string
|
||||||
format to a JavaScript Date object.
|
format to a JavaScript Date object.
|
||||||
*/
|
*/
|
||||||
const fromRailsStringToJavascriptDate = date => {
|
export const fromRailsStringToJavascriptDate = date => {
|
||||||
let dateOnly = date.slice(0, 10);
|
let dateOnly = date.slice(0, 10);
|
||||||
let timeOnly = date.slice(11, 19);
|
let timeOnly = date.slice(11, 19);
|
||||||
|
|
||||||
return new Date(`${dateOnly}T${timeOnly}Z`);
|
return new Date(`${dateOnly}T${timeOnly}Z`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fromJavascriptDateToRailsString = (date: Date) => {
|
||||||
|
return date.toJSON();
|
||||||
}
|
}
|
||||||
8
app/javascript/interfaces/IPostStatusChange.ts
Normal file
8
app/javascript/interfaces/IPostStatusChange.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
interface IPostStatusChange {
|
||||||
|
postStatusId: number;
|
||||||
|
userFullName: string;
|
||||||
|
userEmail: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IPostStatusChange;
|
||||||
7
app/javascript/interfaces/json/IFollow.ts
Normal file
7
app/javascript/interfaces/json/IFollow.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
interface IFollowJSON {
|
||||||
|
id: number;
|
||||||
|
user_id: number;
|
||||||
|
post_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IFollowJSON;
|
||||||
8
app/javascript/interfaces/json/IPostStatusChange.ts
Normal file
8
app/javascript/interfaces/json/IPostStatusChange.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
interface IPostStatusChangeJSON {
|
||||||
|
post_status_id: number;
|
||||||
|
user_full_name: string;
|
||||||
|
user_email: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IPostStatusChangeJSON;
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
HandleCommentRepliesType,
|
HandleCommentRepliesType,
|
||||||
TOGGLE_COMMENT_REPLY,
|
TOGGLE_COMMENT_REPLY,
|
||||||
SET_COMMENT_REPLY_BODY,
|
SET_COMMENT_REPLY_BODY,
|
||||||
|
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||||
} from '../actions/Comment/handleCommentReplies';
|
} from '../actions/Comment/handleCommentReplies';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -82,6 +83,7 @@ const commentsReducer = (
|
|||||||
|
|
||||||
case TOGGLE_COMMENT_REPLY:
|
case TOGGLE_COMMENT_REPLY:
|
||||||
case SET_COMMENT_REPLY_BODY:
|
case SET_COMMENT_REPLY_BODY:
|
||||||
|
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
replyForms: replyFormsReducer(state.replyForms, action),
|
replyForms: replyFormsReducer(state.replyForms, action),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
HandleCommentRepliesType,
|
HandleCommentRepliesType,
|
||||||
TOGGLE_COMMENT_REPLY,
|
TOGGLE_COMMENT_REPLY,
|
||||||
SET_COMMENT_REPLY_BODY,
|
SET_COMMENT_REPLY_BODY,
|
||||||
|
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||||
} from '../actions/Comment/handleCommentReplies';
|
} from '../actions/Comment/handleCommentReplies';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -52,21 +53,40 @@ import {
|
|||||||
TOGGLE_COMMENT_IS_UPDATE_SUCCESS,
|
TOGGLE_COMMENT_IS_UPDATE_SUCCESS,
|
||||||
} from '../actions/Comment/updateComment';
|
} from '../actions/Comment/updateComment';
|
||||||
|
|
||||||
|
import { FollowActionTypes, FOLLOW_SUBMIT_SUCCESS } from '../actions/Follow/submitFollow';
|
||||||
|
import { FollowRequestActionTypes, FOLLOW_REQUEST_SUCCESS } from '../actions/Follow/requestFollow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PostStatusChangesRequestActionTypes,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_START,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_SUCCESS,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_FAILURE,
|
||||||
|
} from '../actions/PostStatusChange/requestPostStatusChanges';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PostStatusChangeSubmitted,
|
||||||
|
POST_STATUS_CHANGE_SUBMITTED
|
||||||
|
} from '../actions/PostStatusChange/submittedPostStatusChange';
|
||||||
|
|
||||||
import postReducer from './postReducer';
|
import postReducer from './postReducer';
|
||||||
import likesReducer from './likesReducer';
|
import likesReducer from './likesReducer';
|
||||||
import commentsReducer from './commentsReducer';
|
import commentsReducer from './commentsReducer';
|
||||||
|
|
||||||
import { LikesState } from './likesReducer';
|
import { LikesState } from './likesReducer';
|
||||||
import { CommentsState } from './commentsReducer';
|
import { CommentsState } from './commentsReducer';
|
||||||
|
import postStatusChangesReducer, { PostStatusChangesState } from './postStatusChangesReducer';
|
||||||
|
|
||||||
import IPost from '../interfaces/IPost';
|
import IPost from '../interfaces/IPost';
|
||||||
|
|
||||||
|
|
||||||
interface CurrentPostState {
|
interface CurrentPostState {
|
||||||
item: IPost;
|
item: IPost;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
likes: LikesState;
|
likes: LikesState;
|
||||||
|
followed: boolean;
|
||||||
comments: CommentsState;
|
comments: CommentsState;
|
||||||
|
postStatusChanges: PostStatusChangesState,
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CurrentPostState = {
|
const initialState: CurrentPostState = {
|
||||||
@@ -74,7 +94,9 @@ const initialState: CurrentPostState = {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: '',
|
error: '',
|
||||||
likes: likesReducer(undefined, {} as LikesRequestActionTypes),
|
likes: likesReducer(undefined, {} as LikesRequestActionTypes),
|
||||||
|
followed: false,
|
||||||
comments: commentsReducer(undefined, {} as CommentsRequestActionTypes),
|
comments: commentsReducer(undefined, {} as CommentsRequestActionTypes),
|
||||||
|
postStatusChanges: postStatusChangesReducer(undefined, {} as PostStatusChangesRequestActionTypes),
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPostReducer = (
|
const currentPostReducer = (
|
||||||
@@ -88,7 +110,11 @@ const currentPostReducer = (
|
|||||||
CommentsRequestActionTypes |
|
CommentsRequestActionTypes |
|
||||||
HandleCommentRepliesType |
|
HandleCommentRepliesType |
|
||||||
CommentSubmitActionTypes |
|
CommentSubmitActionTypes |
|
||||||
ToggleIsUpdateSuccessAction
|
ToggleIsUpdateSuccessAction |
|
||||||
|
FollowActionTypes |
|
||||||
|
FollowRequestActionTypes |
|
||||||
|
PostStatusChangesRequestActionTypes |
|
||||||
|
PostStatusChangeSubmitted
|
||||||
): CurrentPostState => {
|
): CurrentPostState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case POST_REQUEST_START:
|
case POST_REQUEST_START:
|
||||||
@@ -137,11 +163,33 @@ const currentPostReducer = (
|
|||||||
case COMMENT_SUBMIT_SUCCESS:
|
case COMMENT_SUBMIT_SUCCESS:
|
||||||
case COMMENT_SUBMIT_FAILURE:
|
case COMMENT_SUBMIT_FAILURE:
|
||||||
case TOGGLE_COMMENT_IS_UPDATE_SUCCESS:
|
case TOGGLE_COMMENT_IS_UPDATE_SUCCESS:
|
||||||
|
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
comments: commentsReducer(state.comments, action),
|
comments: commentsReducer(state.comments, action),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case FOLLOW_REQUEST_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
followed: action.follow.user_id ? true : false,
|
||||||
|
};
|
||||||
|
|
||||||
|
case FOLLOW_SUBMIT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
followed: action.isFollow,
|
||||||
|
};
|
||||||
|
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_START:
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_SUCCESS:
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_FAILURE:
|
||||||
|
case POST_STATUS_CHANGE_SUBMITTED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
postStatusChanges: postStatusChangesReducer(state.postStatusChanges, action),
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
71
app/javascript/reducers/postStatusChangesReducer.ts
Normal file
71
app/javascript/reducers/postStatusChangesReducer.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
PostStatusChangesRequestActionTypes,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_START,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_SUCCESS,
|
||||||
|
POST_STATUS_CHANGES_REQUEST_FAILURE,
|
||||||
|
} from "../actions/PostStatusChange/requestPostStatusChanges";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PostStatusChangeSubmitted,
|
||||||
|
POST_STATUS_CHANGE_SUBMITTED
|
||||||
|
} from '../actions/PostStatusChange/submittedPostStatusChange';
|
||||||
|
|
||||||
|
import IPostStatusChange from "../interfaces/IPostStatusChange";
|
||||||
|
|
||||||
|
export interface PostStatusChangesState {
|
||||||
|
items: Array<IPostStatusChange>;
|
||||||
|
areLoading: boolean;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: PostStatusChangesState = {
|
||||||
|
items: [],
|
||||||
|
areLoading: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const postStatusChangesReducer = (
|
||||||
|
state = initialState,
|
||||||
|
action:
|
||||||
|
PostStatusChangesRequestActionTypes |
|
||||||
|
PostStatusChangeSubmitted
|
||||||
|
) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_START:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: action.postStatusChanges.map(postStatusChange => ({
|
||||||
|
postStatusId: postStatusChange.post_status_id,
|
||||||
|
userFullName: postStatusChange.user_full_name,
|
||||||
|
userEmail: postStatusChange.user_email,
|
||||||
|
updatedAt: postStatusChange.updated_at,
|
||||||
|
})),
|
||||||
|
areLoading: false,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
case POST_STATUS_CHANGES_REQUEST_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
areLoading: false,
|
||||||
|
error: action.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
case POST_STATUS_CHANGE_SUBMITTED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
items: [action.postStatusChange, ...state.items],
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default postStatusChangesReducer;
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
HandleCommentRepliesType,
|
HandleCommentRepliesType,
|
||||||
TOGGLE_COMMENT_REPLY,
|
TOGGLE_COMMENT_REPLY,
|
||||||
SET_COMMENT_REPLY_BODY,
|
SET_COMMENT_REPLY_BODY,
|
||||||
|
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||||
} from '../actions/Comment/handleCommentReplies';
|
} from '../actions/Comment/handleCommentReplies';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +21,7 @@ export interface ReplyFormState {
|
|||||||
commentId: number;
|
commentId: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
body: string;
|
body: string;
|
||||||
|
isPostUpdate: boolean;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
@@ -28,6 +30,7 @@ const initialState: ReplyFormState = {
|
|||||||
commentId: undefined,
|
commentId: undefined,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
body: '',
|
body: '',
|
||||||
|
isPostUpdate: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
error: '',
|
error: '',
|
||||||
}
|
}
|
||||||
@@ -58,6 +61,12 @@ const replyFormReducer = (
|
|||||||
body: action.body,
|
body: action.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isPostUpdate: !state.isPostUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
case COMMENT_SUBMIT_START:
|
case COMMENT_SUBMIT_START:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -69,6 +78,7 @@ const replyFormReducer = (
|
|||||||
...state,
|
...state,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
body: '',
|
body: '',
|
||||||
|
isPostUpdate: false,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
HandleCommentRepliesType,
|
HandleCommentRepliesType,
|
||||||
TOGGLE_COMMENT_REPLY,
|
TOGGLE_COMMENT_REPLY,
|
||||||
SET_COMMENT_REPLY_BODY,
|
SET_COMMENT_REPLY_BODY,
|
||||||
|
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||||
} from '../actions/Comment/handleCommentReplies';
|
} from '../actions/Comment/handleCommentReplies';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +43,7 @@ const ReplyFormsReducer = (
|
|||||||
|
|
||||||
case TOGGLE_COMMENT_REPLY:
|
case TOGGLE_COMMENT_REPLY:
|
||||||
case SET_COMMENT_REPLY_BODY:
|
case SET_COMMENT_REPLY_BODY:
|
||||||
|
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||||
return (
|
return (
|
||||||
state.map(
|
state.map(
|
||||||
replyForm => (
|
replyForm => (
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
.commentsContainer {
|
.commentsContainer {
|
||||||
|
@extend .my-3;
|
||||||
|
|
||||||
.newCommentForm {
|
.newCommentForm {
|
||||||
@extend
|
@extend
|
||||||
.d-flex,
|
.d-flex,
|
||||||
|
.flex-column,
|
||||||
.my-3;
|
.my-3;
|
||||||
|
|
||||||
|
.commentBodyForm {
|
||||||
|
@extend .d-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentIsUpdateForm {
|
||||||
|
@extend
|
||||||
|
.d-flex,
|
||||||
|
.justify-content-between,
|
||||||
|
.mt-3;
|
||||||
|
|
||||||
|
margin-left: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
.currentUserAvatar {
|
.currentUserAvatar {
|
||||||
@extend
|
@extend
|
||||||
.gravatar,
|
.gravatar,
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
@extend .sidebarCard;
|
@extend .sidebarCard;
|
||||||
|
|
||||||
.postUpdateList {
|
.postUpdateList {
|
||||||
@extend .w-100;
|
@extend
|
||||||
|
.scroll-shadows,
|
||||||
|
.w-100;
|
||||||
|
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -26,8 +28,16 @@
|
|||||||
@extend
|
@extend
|
||||||
.d-flex,
|
.d-flex,
|
||||||
.flex-column,
|
.flex-column,
|
||||||
.p-2,
|
.p-2;
|
||||||
.my-1;
|
|
||||||
|
&:after { // displays the little centered border under each post update
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 25%;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.postUpdateListItemHeader {
|
.postUpdateListItemHeader {
|
||||||
@extend .d-flex;
|
@extend .d-flex;
|
||||||
@@ -42,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.postUpdateListItemBody {
|
.postUpdateListItemBody {
|
||||||
@extend .m-0;
|
@extend .my-1;
|
||||||
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
@@ -54,7 +64,9 @@
|
|||||||
@extend .sidebarCard;
|
@extend .sidebarCard;
|
||||||
|
|
||||||
.likeList {
|
.likeList {
|
||||||
@extend .w-100;
|
@extend
|
||||||
|
.scroll-shadows,
|
||||||
|
.w-100;
|
||||||
|
|
||||||
max-height: 170px;
|
max-height: 170px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
@@ -69,7 +81,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionBoxContainer {
|
||||||
|
@extend
|
||||||
|
.sidebarCard,
|
||||||
|
.text-center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@extend
|
||||||
|
.mt-3,
|
||||||
|
.mb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.postAndCommentsContainer {
|
.postAndCommentsContainer {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ a {
|
|||||||
.mb-3,
|
.mb-3,
|
||||||
.p-2;
|
.p-2;
|
||||||
|
|
||||||
width: 250px;
|
width: 280px;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
color: $muted-text-color;
|
color: $muted-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smallMutedText {
|
||||||
|
@extend
|
||||||
|
.mutedText,
|
||||||
|
.m-0;
|
||||||
|
|
||||||
|
font-size: smaller;
|
||||||
|
line-height: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
.uppercaseText {
|
.uppercaseText {
|
||||||
@extend
|
@extend
|
||||||
.text-secondary,
|
.text-secondary,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 920px;
|
max-width: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.turbolinks-progress-bar {
|
.turbolinks-progress-bar {
|
||||||
@@ -34,4 +34,41 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #343a40;
|
background-color: #343a40;
|
||||||
border-color: #343a40;
|
border-color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credits: https://codepen.io/chriscoyier/pen/YzXBYvL
|
||||||
|
.scroll-shadows {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
background:
|
||||||
|
/* Shadow Cover TOP */
|
||||||
|
linear-gradient(
|
||||||
|
white 30%,
|
||||||
|
rgba(255, 255, 255, 0)
|
||||||
|
) center top,
|
||||||
|
|
||||||
|
/* Shadow Cover BOTTOM */
|
||||||
|
linear-gradient(
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
white 70%
|
||||||
|
) center bottom,
|
||||||
|
|
||||||
|
/* Shadow TOP */
|
||||||
|
radial-gradient(
|
||||||
|
farthest-side at 50% 0,
|
||||||
|
rgba(0, 0, 0, 0.2),
|
||||||
|
rgba(0, 0, 0, 0)
|
||||||
|
) center top,
|
||||||
|
|
||||||
|
/* Shadow BOTTOM */
|
||||||
|
radial-gradient(
|
||||||
|
farthest-side at 50% 100%,
|
||||||
|
rgba(0, 0, 0, 0.2),
|
||||||
|
rgba(0, 0, 0, 0)
|
||||||
|
) center bottom;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 40px, 100% 40px, 100% 10px, 100% 10px;
|
||||||
|
background-attachment: local, local, scroll, scroll;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: 'from@example.com'
|
default from: "notifications@example.com"
|
||||||
layout 'mailer'
|
layout 'mailer'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,45 @@
|
|||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
default from: "notifications@example.com"
|
|
||||||
|
|
||||||
def notify_post_owner(comment:)
|
def notify_post_owner(comment:)
|
||||||
@comment = comment
|
@comment = comment
|
||||||
@user = comment.post.user
|
@user = comment.post.user
|
||||||
|
|
||||||
mail(to: @user.email, subject: "[#{ENV.fetch('APP_NAME')}] - New comment on #{comment.post.title}")
|
mail(
|
||||||
|
to: @user.email,
|
||||||
|
subject: "[#{app_name}] New comment on #{comment.post.title}"
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notify_comment_owner(comment:)
|
||||||
|
@comment = comment
|
||||||
|
@user = comment.parent.user
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: @user.email,
|
||||||
|
subject: "[#{app_name}] New reply on your comment from #{comment.post.title}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_followers_of_post_update(comment:)
|
||||||
|
@comment = comment
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: comment.post.followers.pluck(:email),
|
||||||
|
subject: "[#{app_name}] New update on #{comment.post.title}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def notify_followers_of_post_status_change(post:)
|
||||||
|
@post = post
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: post.followers.pluck(:email),
|
||||||
|
subject: "[#{app_name}] Status change on post #{post.title}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def app_name
|
||||||
|
ENV.fetch('APP_NAME')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
6
app/models/follow.rb
Normal file
6
app/models/follow.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class Follow < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :post
|
||||||
|
|
||||||
|
validates :user_id, uniqueness: { scope: :post_id }
|
||||||
|
end
|
||||||
@@ -2,8 +2,12 @@ class Post < ApplicationRecord
|
|||||||
belongs_to :board
|
belongs_to :board
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :post_status, optional: true
|
belongs_to :post_status, optional: true
|
||||||
|
|
||||||
has_many :likes, dependent: :destroy
|
has_many :likes, dependent: :destroy
|
||||||
|
has_many :follows, dependent: :destroy
|
||||||
|
has_many :followers, through: :follows, source: :user
|
||||||
has_many :comments, dependent: :destroy
|
has_many :comments, dependent: :destroy
|
||||||
|
has_many :post_status_changes, dependent: :destroy
|
||||||
|
|
||||||
validates :title, presence: true, length: { in: 4..64 }
|
validates :title, presence: true, length: { in: 4..64 }
|
||||||
|
|
||||||
|
|||||||
5
app/models/post_status_change.rb
Normal file
5
app/models/post_status_change.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class PostStatusChange < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :post
|
||||||
|
belongs_to :post_status, optional: true
|
||||||
|
end
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= f.label :notifications_enabled %> <br />
|
<%= f.label :notifications_enabled %>
|
||||||
<%= f.check_box :notifications_enabled, style: "transform: scale(1.5)" %>
|
<%= f.check_box :notifications_enabled, style: "transform: scale(1.5)" %>
|
||||||
<small id="notificationsHelp" class="form-text text-muted">
|
<small id="notificationsHelp" class="form-text text-muted">
|
||||||
if disabled, you won't receive any notification
|
if disabled, you won't receive any notification
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
postStatuses: @post_statuses,
|
postStatuses: @post_statuses,
|
||||||
isLoggedIn: user_signed_in?,
|
isLoggedIn: user_signed_in?,
|
||||||
isPowerUser: user_signed_in? ? current_user.power_user? : false,
|
isPowerUser: user_signed_in? ? current_user.power_user? : false,
|
||||||
|
userFullName: user_signed_in? ? current_user.full_name : nil,
|
||||||
userEmail: user_signed_in? ? current_user.email : nil,
|
userEmail: user_signed_in? ? current_user.email : nil,
|
||||||
authenticityToken: form_authenticity_token,
|
authenticityToken: form_authenticity_token,
|
||||||
}
|
}
|
||||||
|
|||||||
24
app/views/user_mailer/notify_comment_owner.html.erb
Normal file
24
app/views/user_mailer/notify_comment_owner.html.erb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, <%= @user.full_name %></h1>
|
||||||
|
<p>
|
||||||
|
There is a new reply by <b><%= @comment.user.full_name %></b> on your comment from post <b><%= @comment.post.title %></b>:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i><%= @comment.body %></i>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Have a great day!
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
<p>
|
||||||
|
The post you're following <b><%= @post.title %></b> has a new status:
|
||||||
|
<span style='background-color: <%= @post.post_status.color %>; color: white;'%>>
|
||||||
|
<%= @post.post_status.name %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= link_to "Click here", post_url(@post) %> to learn more!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Have a great day!
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
24
app/views/user_mailer/notify_followers_of_post_update.erb
Normal file
24
app/views/user_mailer/notify_followers_of_post_update.erb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
<p>
|
||||||
|
There is a new update on the post you're following <b><%= @comment.post.title %></b>:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i><%= @comment.body %></i>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Have a great day!
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
<footer>
|
||||||
|
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
|
||||||
|
</footer>
|
||||||
|
</html>
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
|
<meta content='text/html; charset=utf-8' http-equiv='content-type' />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Hello, <%= @user.full_name %></h1>
|
<h1>Hello, <%= @user.full_name %></h1>
|
||||||
<p>
|
<p>
|
||||||
There is a new comment by <%= @comment.user.full_name %> on your post <b><%= @comment.post.title %></b>
|
There is a new comment by <b><%= @comment.user.full_name %></b> on your post <b><%= @comment.post.title %></b>:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i><%= @comment.body %></i>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<%= link_to "Click here", post_url(@comment.post) %> to have your say!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Have a great day!
|
||||||
</p>
|
</p>
|
||||||
<p> To see this comment, <%= link_to "Click here", post_url(@comment.post) %> </p>
|
|
||||||
<p>Have a great day!</p>
|
|
||||||
</body>
|
</body>
|
||||||
<footer>
|
<footer>
|
||||||
Annoyed ? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>
|
Annoyed? You can <%= link_to("turn off notifications here", edit_user_registration_url) %>.
|
||||||
</footer>
|
</footer>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ default: &default
|
|||||||
host: db
|
host: db
|
||||||
username: <%= ENV['POSTGRES_USER'] %>
|
username: <%= ENV['POSTGRES_USER'] %>
|
||||||
password: <%= ENV['POSTGRES_PASSWORD'] %>
|
password: <%= ENV['POSTGRES_PASSWORD'] %>
|
||||||
database: <%= ENV['POSTGRES_USER'] %>
|
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
|
||||||
|
database: <%= ENV['POSTGRES_USER'] %>
|
||||||
|
|
||||||
# The specified database role being used to connect to postgres.
|
# The specified database role being used to connect to postgres.
|
||||||
# To create additional roles in postgres see `$ createuser --help`.
|
# To create additional roles in postgres see `$ createuser --help`.
|
||||||
# When left blank, postgres will use the default role. This is
|
# When left blank, postgres will use the default role. This is
|
||||||
@@ -57,7 +59,8 @@ development:
|
|||||||
# Do not set this db to the same as development or production.
|
# Do not set this db to the same as development or production.
|
||||||
test:
|
test:
|
||||||
<<: *default
|
<<: *default
|
||||||
database: app_test
|
|
||||||
|
database: <%= ENV['POSTGRES_USER'] %>_test
|
||||||
|
|
||||||
# As with config/credentials.yml, you never want to store sensitive information,
|
# As with config/credentials.yml, you never want to store sensitive information,
|
||||||
# like your database password, in your source code. If your source code is
|
# like your database password, in your source code. If your source code is
|
||||||
@@ -80,3 +83,5 @@ test:
|
|||||||
#
|
#
|
||||||
production:
|
production:
|
||||||
<<: *default
|
<<: *default
|
||||||
|
|
||||||
|
database: <%= ENV['POSTGRES_USER'] %>
|
||||||
@@ -15,9 +15,12 @@ Rails.application.routes.draw do
|
|||||||
devise_for :users
|
devise_for :users
|
||||||
|
|
||||||
resources :posts, only: [:index, :create, :show, :update] do
|
resources :posts, only: [:index, :create, :show, :update] do
|
||||||
|
resource :follows, only: [:create, :destroy]
|
||||||
|
resources :follows, only: [:index]
|
||||||
resource :likes, only: [:create, :destroy]
|
resource :likes, only: [:create, :destroy]
|
||||||
resources :likes, only: [:index]
|
resources :likes, only: [:index]
|
||||||
resources :comments, only: [:index, :create, :update]
|
resources :comments, only: [:index, :create, :update]
|
||||||
|
resources :post_status_changes, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :boards, only: [:index, :create, :update, :destroy, :show] do
|
resources :boards, only: [:index, :create, :update, :destroy, :show] do
|
||||||
|
|||||||
12
db/migrate/20220512184400_create_follows.rb
Normal file
12
db/migrate/20220512184400_create_follows.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
class CreateFollows < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :follows do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.references :post, null: false, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :follows, [:user_id, :post_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
11
db/migrate/20220521161950_create_post_status_changes.rb
Normal file
11
db/migrate/20220521161950_create_post_status_changes.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class CreatePostStatusChanges < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :post_status_changes do |t|
|
||||||
|
t.references :user, null: false, foreign_key: true
|
||||||
|
t.references :post, null: false, foreign_key: true
|
||||||
|
t.references :post_status, foreign_key: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
28
db/schema.rb
28
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_01_26_215831) do
|
ActiveRecord::Schema.define(version: 2022_05_21_161950) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@@ -37,6 +37,16 @@ ActiveRecord::Schema.define(version: 2021_01_26_215831) do
|
|||||||
t.index ["user_id"], name: "index_comments_on_user_id"
|
t.index ["user_id"], name: "index_comments_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "follows", force: :cascade do |t|
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "post_id", null: false
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["post_id"], name: "index_follows_on_post_id"
|
||||||
|
t.index ["user_id", "post_id"], name: "index_follows_on_user_id_and_post_id", unique: true
|
||||||
|
t.index ["user_id"], name: "index_follows_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "likes", force: :cascade do |t|
|
create_table "likes", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
@@ -47,6 +57,17 @@ ActiveRecord::Schema.define(version: 2021_01_26_215831) do
|
|||||||
t.index ["user_id"], name: "index_likes_on_user_id"
|
t.index ["user_id"], name: "index_likes_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "post_status_changes", force: :cascade do |t|
|
||||||
|
t.bigint "user_id", null: false
|
||||||
|
t.bigint "post_id", null: false
|
||||||
|
t.bigint "post_status_id"
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["post_id"], name: "index_post_status_changes_on_post_id"
|
||||||
|
t.index ["post_status_id"], name: "index_post_status_changes_on_post_status_id"
|
||||||
|
t.index ["user_id"], name: "index_post_status_changes_on_user_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "post_statuses", force: :cascade do |t|
|
create_table "post_statuses", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "color", null: false
|
t.string "color", null: false
|
||||||
@@ -93,8 +114,13 @@ ActiveRecord::Schema.define(version: 2021_01_26_215831) do
|
|||||||
add_foreign_key "comments", "comments", column: "parent_id"
|
add_foreign_key "comments", "comments", column: "parent_id"
|
||||||
add_foreign_key "comments", "posts"
|
add_foreign_key "comments", "posts"
|
||||||
add_foreign_key "comments", "users"
|
add_foreign_key "comments", "users"
|
||||||
|
add_foreign_key "follows", "posts"
|
||||||
|
add_foreign_key "follows", "users"
|
||||||
add_foreign_key "likes", "posts"
|
add_foreign_key "likes", "posts"
|
||||||
add_foreign_key "likes", "users"
|
add_foreign_key "likes", "users"
|
||||||
|
add_foreign_key "post_status_changes", "post_statuses"
|
||||||
|
add_foreign_key "post_status_changes", "posts"
|
||||||
|
add_foreign_key "post_status_changes", "users"
|
||||||
add_foreign_key "posts", "boards"
|
add_foreign_key "posts", "boards"
|
||||||
add_foreign_key "posts", "post_statuses"
|
add_foreign_key "posts", "post_statuses"
|
||||||
add_foreign_key "posts", "users"
|
add_foreign_key "posts", "users"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
- POSTGRES_USER
|
- POSTGRES_USER
|
||||||
- POSTGRES_PASSWORD
|
- POSTGRES_PASSWORD
|
||||||
volumes:
|
volumes:
|
||||||
- ./tmp/db:/var/lib/postgresql/data
|
- dbdata:/var/lib/postgresql/data
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -26,4 +26,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dbdata:
|
||||||
6
spec/factories/follows.rb
Normal file
6
spec/factories/follows.rb
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :follow do
|
||||||
|
user
|
||||||
|
post
|
||||||
|
end
|
||||||
|
end
|
||||||
7
spec/factories/post_status_changes.rb
Normal file
7
spec/factories/post_status_changes.rb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory :post_status_change do
|
||||||
|
user
|
||||||
|
post
|
||||||
|
post_status
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,15 +8,16 @@ RSpec.describe UserMailer, type: :mailer do
|
|||||||
let(:mail) { UserMailer.notify_post_owner(comment: comment) }
|
let(:mail) { UserMailer.notify_post_owner(comment: comment) }
|
||||||
|
|
||||||
it "renders the headers" do
|
it "renders the headers" do
|
||||||
expect(mail.subject).to eq("[#{ENV.fetch('APP_NAME')}] - New comment on #{post.title}")
|
expect(mail.subject).to eq("[#{ENV.fetch('APP_NAME')}] New comment on #{post.title}")
|
||||||
expect(mail.to).to eq(["notified@example.com"])
|
expect(mail.to).to eq(["notified@example.com"])
|
||||||
expect(mail.from).to eq(["notifications@example.com"])
|
expect(mail.from).to eq(["notifications@example.com"])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "renders the body" do
|
it "renders the user name, post title, replier name and comment body" do
|
||||||
expect(mail.body.encoded).to include("Hello, #{user.full_name}")
|
expect(mail.body.encoded).to include(user.full_name)
|
||||||
expect(mail.body.encoded).to include("There is a new comment by")
|
expect(mail.body.encoded).to include(post.title)
|
||||||
expect(mail.body.encoded).to include('Annoyed ? You can <a href="http://localhost:3000/users/edit">turn off notifications here</a>')
|
expect(mail.body.encoded).to include(comment.user.full_name)
|
||||||
|
expect(mail.body.encoded).to include(comment.body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
26
spec/models/follow_spec.rb
Normal file
26
spec/models/follow_spec.rb
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Follow, type: :model do
|
||||||
|
let(:follow) { FactoryBot.build(:follow) }
|
||||||
|
|
||||||
|
it 'is valid' do
|
||||||
|
expect(follow).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have a user_id' do
|
||||||
|
follow.user = nil
|
||||||
|
expect(follow).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have a post_id' do
|
||||||
|
follow.post = nil
|
||||||
|
expect(follow).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must be unique on user and post' do
|
||||||
|
follow
|
||||||
|
f = Follow.new(user_id: follow.user_id, post_id: follow.post_id)
|
||||||
|
|
||||||
|
expect(f).to be_invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
24
spec/models/post_status_change_spec.rb
Normal file
24
spec/models/post_status_change_spec.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe PostStatusChange, type: :model do
|
||||||
|
let(:post_status_change) { FactoryBot.build(:post_status_change) }
|
||||||
|
|
||||||
|
it 'should be valid' do
|
||||||
|
expect(post_status_change).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have a post' do
|
||||||
|
post_status_change.post = nil
|
||||||
|
expect(post_status_change).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have a user' do
|
||||||
|
post_status_change.user = nil
|
||||||
|
expect(post_status_change).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can have a null post status' do
|
||||||
|
post_status_change.post_status = nil
|
||||||
|
expect(post_status_change).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user