mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +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
@@ -11,6 +11,12 @@ interface SetCommentReplyBodyAction {
|
||||
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 => ({
|
||||
type: TOGGLE_COMMENT_REPLY,
|
||||
commentId,
|
||||
@@ -22,6 +28,12 @@ export const setCommentReplyBody = (commentId: number, body: string): SetComment
|
||||
body,
|
||||
});
|
||||
|
||||
export const toggleCommentIsPostUpdateFlag = (commentId: number): ToggleCommentIsPostUpdateFlag => ({
|
||||
type: TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||
commentId,
|
||||
});
|
||||
|
||||
export type HandleCommentRepliesType =
|
||||
ToggleCommentReplyAction |
|
||||
SetCommentReplyBodyAction;
|
||||
SetCommentReplyBodyAction |
|
||||
ToggleCommentIsPostUpdateFlag;
|
||||
@@ -52,6 +52,7 @@ export const submitComment = (
|
||||
postId: number,
|
||||
body: string,
|
||||
parentId: number,
|
||||
isPostUpdate: boolean,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(commentSubmitStart(parentId));
|
||||
@@ -64,6 +65,7 @@ export const submitComment = (
|
||||
comment: {
|
||||
body,
|
||||
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 friendlyDate from '../../helpers/friendlyDate';
|
||||
import friendlyDate from '../../helpers/datetime';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
handleToggleCommentReply(): void;
|
||||
handleCommentReplyBodyChange(e: React.FormEvent): void;
|
||||
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
||||
handleSubmitComment(body: string, parentId: number): void;
|
||||
handleSubmitComment(body: string, parentId: number, isPostUpdate: boolean): void;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
@@ -48,7 +48,7 @@ const Comment = ({
|
||||
}: Props) => (
|
||||
<div className="comment">
|
||||
<div className="commentHeader">
|
||||
<Gravatar email={userEmail} size={24} className="gravatar" />
|
||||
<Gravatar email={userEmail} size={28} className="gravatar" />
|
||||
<span className="commentAuthor">{userFullName}</span>
|
||||
{ isPostUpdate ? <span className="postUpdateBadge">Post update</span> : null }
|
||||
</div>
|
||||
@@ -89,12 +89,15 @@ const Comment = ({
|
||||
<NewComment
|
||||
body={replyForm.body}
|
||||
parentId={id}
|
||||
postUpdateFlagValue={replyForm.isPostUpdate}
|
||||
isSubmitting={replyForm.isSubmitting}
|
||||
error={replyForm.error}
|
||||
handleChange={handleCommentReplyBodyChange}
|
||||
handlePostUpdateFlag={() => null}
|
||||
handleSubmit={handleSubmitComment}
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPowerUser={isPowerUser}
|
||||
userEmail={currentUserEmail}
|
||||
/>
|
||||
:
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
toggleCommentReply(commentId: number): void;
|
||||
setCommentReplyBody(commentId: number, body: string): void;
|
||||
handleToggleIsCommentUpdate(commentId: number, currentIsPostUpdate: boolean): void;
|
||||
handleSubmitComment(body: string, parentId: number): void;
|
||||
handleSubmitComment(body: string, parentId: number, isPostUpdate: boolean): void;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
requestComments(postId: number, page?: number): void;
|
||||
toggleCommentReply(commentId: number): void;
|
||||
setCommentReplyBody(commentId: number, body: string): void;
|
||||
toggleCommentIsPostUpdateFlag(): void;
|
||||
toggleCommentIsPostUpdate(
|
||||
postId: number,
|
||||
commentId: number,
|
||||
@@ -33,6 +34,7 @@ interface Props {
|
||||
postId: number,
|
||||
body: string,
|
||||
parentId: number,
|
||||
isPostUpdate: boolean,
|
||||
authenticityToken: string,
|
||||
): 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.postId,
|
||||
body,
|
||||
parentId,
|
||||
isPostUpdate,
|
||||
this.props.authenticityToken,
|
||||
);
|
||||
}
|
||||
@@ -73,6 +76,7 @@ class CommentsP extends React.Component<Props> {
|
||||
|
||||
toggleCommentReply,
|
||||
setCommentReplyBody,
|
||||
toggleCommentIsPostUpdateFlag,
|
||||
} = this.props;
|
||||
|
||||
const postReply = replyForms.find(replyForm => replyForm.commentId === null);
|
||||
@@ -82,6 +86,7 @@ class CommentsP extends React.Component<Props> {
|
||||
<NewComment
|
||||
body={postReply && postReply.body}
|
||||
parentId={null}
|
||||
postUpdateFlagValue={postReply && postReply.isPostUpdate}
|
||||
isSubmitting={postReply && postReply.isSubmitting}
|
||||
error={postReply && postReply.error}
|
||||
handleChange={
|
||||
@@ -89,9 +94,11 @@ class CommentsP extends React.Component<Props> {
|
||||
setCommentReplyBody(null, (e.target as HTMLTextAreaElement).value)
|
||||
)
|
||||
}
|
||||
handlePostUpdateFlag={toggleCommentIsPostUpdateFlag}
|
||||
handleSubmit={this._handleSubmitComment}
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPowerUser={isPowerUser}
|
||||
userEmail={userEmail}
|
||||
/>
|
||||
|
||||
@@ -99,7 +106,7 @@ class CommentsP extends React.Component<Props> {
|
||||
{ error ? <DangerText>{error}</DangerText> : null }
|
||||
|
||||
<div className="commentsTitle">
|
||||
activity • {comments.length} comments
|
||||
activity • {comments.length} comment{comments.length === 1 ? '' : 's'}
|
||||
</div>
|
||||
|
||||
<CommentList
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import Gravatar from 'react-gravatar';
|
||||
|
||||
import NewCommentUpdateSection from './NewCommentUpdateSection';
|
||||
|
||||
import Button from '../shared/Button';
|
||||
import Spinner from '../shared/Spinner';
|
||||
import { DangerText } from '../shared/CustomTexts';
|
||||
@@ -8,24 +10,34 @@ import { DangerText } from '../shared/CustomTexts';
|
||||
interface Props {
|
||||
body: string;
|
||||
parentId: number;
|
||||
postUpdateFlagValue: boolean;
|
||||
isSubmitting: boolean;
|
||||
error: string;
|
||||
handleChange(e: React.FormEvent): void;
|
||||
handleSubmit(body: string, parentId: number): void;
|
||||
handlePostUpdateFlag(): void;
|
||||
handleSubmit(
|
||||
body: string,
|
||||
parentId: number,
|
||||
isPostUpdate: boolean
|
||||
): void;
|
||||
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
const NewComment = ({
|
||||
body,
|
||||
parentId,
|
||||
postUpdateFlagValue,
|
||||
isSubmitting,
|
||||
error,
|
||||
handleChange,
|
||||
handlePostUpdateFlag,
|
||||
handleSubmit,
|
||||
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
userEmail,
|
||||
}: Props) => (
|
||||
<React.Fragment>
|
||||
@@ -33,18 +45,29 @@ const NewComment = ({
|
||||
{
|
||||
isLoggedIn ?
|
||||
<React.Fragment>
|
||||
<Gravatar email={userEmail} size={36} className="currentUserAvatar" />
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={handleChange}
|
||||
placeholder="Leave a comment"
|
||||
className="newCommentBody"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleSubmit(body, parentId)}
|
||||
className="submitCommentButton">
|
||||
{ isSubmitting ? <Spinner color="light" /> : 'Submit' }
|
||||
</Button>
|
||||
<div className="commentBodyForm">
|
||||
<Gravatar email={userEmail} size={48} className="currentUserAvatar" />
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={handleChange}
|
||||
placeholder="Leave a comment"
|
||||
className="newCommentBody"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => handleSubmit(body, parentId, postUpdateFlagValue)}
|
||||
className="submitCommentButton">
|
||||
{ isSubmitting ? <Spinner color="light" /> : 'Submit' }
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
isPowerUser && parentId == null ?
|
||||
<NewCommentUpdateSection
|
||||
postUpdateFlagValue={postUpdateFlagValue}
|
||||
handlePostUpdateFlag={handlePostUpdateFlag}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</React.Fragment>
|
||||
:
|
||||
<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 IBoard from '../../interfaces/IBoard';
|
||||
|
||||
import PostUpdateList from './PostUpdateList';
|
||||
import LikeList from './LikeList';
|
||||
import ActionBox from './ActionBox';
|
||||
import LikeButton from '../../containers/LikeButton';
|
||||
import PostBoardSelect from './PostBoardSelect';
|
||||
import PostStatusSelect from './PostStatusSelect';
|
||||
@@ -13,25 +15,31 @@ import PostStatusLabel from '../shared/PostStatusLabel';
|
||||
import Comments from '../../containers/Comments';
|
||||
import { MutedText } from '../shared/CustomTexts';
|
||||
|
||||
import friendlyDate from '../../helpers/friendlyDate';
|
||||
import { LikesState } from '../../reducers/likesReducer';
|
||||
import { CommentsState } from '../../reducers/commentsReducer';
|
||||
import PostUpdateList from './PostUpdateList';
|
||||
import { PostStatusChangesState } from '../../reducers/postStatusChangesReducer';
|
||||
|
||||
import friendlyDate, { fromRailsStringToJavascriptDate } from '../../helpers/datetime';
|
||||
|
||||
interface Props {
|
||||
postId: number;
|
||||
post: IPost;
|
||||
likes: LikesState;
|
||||
followed: boolean;
|
||||
comments: CommentsState;
|
||||
postStatusChanges: PostStatusChangesState;
|
||||
boards: Array<IBoard>;
|
||||
postStatuses: Array<IPostStatus>;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
userFullName: string;
|
||||
userEmail: string;
|
||||
authenticityToken: string;
|
||||
|
||||
requestPost(postId: number): void;
|
||||
requestLikes(postId: number): void;
|
||||
requestFollow(postId: number): void;
|
||||
requestPostStatusChanges(postId: number): void;
|
||||
changePostBoard(
|
||||
postId: number,
|
||||
newBoardId: number,
|
||||
@@ -40,38 +48,62 @@ interface Props {
|
||||
changePostStatus(
|
||||
postId: number,
|
||||
newPostStatusId: number,
|
||||
userFullName: string,
|
||||
userEmail: string,
|
||||
authenticityToken: string,
|
||||
): void;
|
||||
submitFollow(
|
||||
postId: number,
|
||||
isFollow: boolean,
|
||||
authenticityToken: string,
|
||||
): void;
|
||||
}
|
||||
|
||||
class PostP extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.requestPost(this.props.postId);
|
||||
this.props.requestLikes(this.props.postId);
|
||||
const {postId} = this.props;
|
||||
|
||||
this.props.requestPost(postId);
|
||||
this.props.requestLikes(postId);
|
||||
this.props.requestFollow(postId);
|
||||
this.props.requestPostStatusChanges(postId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
post,
|
||||
likes,
|
||||
followed,
|
||||
comments,
|
||||
postStatusChanges,
|
||||
boards,
|
||||
postStatuses,
|
||||
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
userFullName,
|
||||
userEmail,
|
||||
authenticityToken,
|
||||
|
||||
changePostBoard,
|
||||
changePostStatus,
|
||||
submitFollow,
|
||||
} = 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 (
|
||||
<div className="pageContainer">
|
||||
<div className="sidebar">
|
||||
<PostUpdateList
|
||||
postUpdates={comments.items.filter(comment => comment.isPostUpdate === true)}
|
||||
postUpdates={postUpdates}
|
||||
postStatuses={postStatuses}
|
||||
areLoading={comments.areLoading}
|
||||
error={comments.error}
|
||||
/>
|
||||
@@ -81,6 +113,13 @@ class PostP extends React.Component<Props> {
|
||||
areLoading={likes.areLoading}
|
||||
error={likes.error}
|
||||
/>
|
||||
|
||||
<ActionBox
|
||||
followed={followed}
|
||||
submitFollow={() => submitFollow(post.id, !followed, authenticityToken)}
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="postAndCommentsContainer">
|
||||
@@ -113,7 +152,8 @@ class PostP extends React.Component<Props> {
|
||||
postStatuses={postStatuses}
|
||||
selectedPostStatusId={post.postStatusId}
|
||||
handleChange={
|
||||
newPostStatusId => changePostStatus(post.id, newPostStatusId, authenticityToken)
|
||||
newPostStatusId =>
|
||||
changePostStatus(post.id, newPostStatusId, userFullName, userEmail, authenticityToken)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,17 +5,22 @@ import { BoxTitleText, DangerText, CenteredMutedText, MutedText } from '../share
|
||||
import Spinner from '../shared/Spinner';
|
||||
|
||||
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 {
|
||||
postUpdates: Array<IComment>;
|
||||
postUpdates: Array<IComment | IPostStatusChange>;
|
||||
postStatuses: Array<IPostStatus>
|
||||
areLoading: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const PostUpdateList = ({
|
||||
postUpdates,
|
||||
postStatuses,
|
||||
areLoading,
|
||||
error,
|
||||
}: Props) => (
|
||||
@@ -33,7 +38,18 @@ const PostUpdateList = ({
|
||||
<span>{postUpdate.userFullName}</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface Props {
|
||||
postStatuses: Array<IPostStatus>;
|
||||
isLoggedIn: boolean;
|
||||
isPowerUser: boolean;
|
||||
userFullName: string;
|
||||
userEmail: string;
|
||||
authenticityToken: string;
|
||||
}
|
||||
@@ -37,6 +38,7 @@ class PostRoot extends React.Component<Props> {
|
||||
postStatuses,
|
||||
isLoggedIn,
|
||||
isPowerUser,
|
||||
userFullName,
|
||||
userEmail,
|
||||
authenticityToken
|
||||
} = this.props;
|
||||
@@ -50,6 +52,7 @@ class PostRoot extends React.Component<Props> {
|
||||
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPowerUser={isPowerUser}
|
||||
userFullName={userFullName}
|
||||
userEmail={userEmail}
|
||||
authenticityToken={authenticityToken}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,10 @@ export const CenteredMutedText = ({ children }: Props) => (
|
||||
<p className="centeredText"><span className="mutedText">{children}</span></p>
|
||||
);
|
||||
|
||||
export const SmallMutedText = ({ children }: Props) => (
|
||||
<p className="smallMutedText">{children}</p>
|
||||
);
|
||||
|
||||
export const UppercaseText = ({ children }: Props) => (
|
||||
<span className="uppercaseText">{children}</span>
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ const PostStatusLabel = ({
|
||||
name,
|
||||
color,
|
||||
}: Props) => (
|
||||
<span className="badge" style={{backgroundColor: color, color: 'white'}}>
|
||||
{name?.toUpperCase()}
|
||||
<span className="badge" style={{backgroundColor: color || 'black', color: 'white'}}>
|
||||
{(name || 'no status').toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { requestComments } from '../actions/Comment/requestComments';
|
||||
import {
|
||||
toggleCommentReply,
|
||||
setCommentReplyBody,
|
||||
toggleCommentIsPostUpdateFlag,
|
||||
} from '../actions/Comment/handleCommentReplies';
|
||||
import { toggleCommentIsUpdate } from '../actions/Comment/updateComment';
|
||||
import { submitComment } from '../actions/Comment/submitComment';
|
||||
@@ -32,6 +33,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setCommentReplyBody(commentId, body));
|
||||
},
|
||||
|
||||
toggleCommentIsPostUpdateFlag() {
|
||||
dispatch(toggleCommentIsPostUpdateFlag(null));
|
||||
},
|
||||
|
||||
toggleCommentIsPostUpdate(
|
||||
postId: number,
|
||||
commentId: number,
|
||||
@@ -45,9 +50,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
postId: number,
|
||||
body: string,
|
||||
parentId: number,
|
||||
isPostUpdate: boolean,
|
||||
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 { changePostBoard } from '../actions/Post/changePostBoard';
|
||||
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 PostP from '../components/Post/PostP';
|
||||
|
||||
import { fromJavascriptDateToRailsString } from '../helpers/datetime';
|
||||
|
||||
const mapStateToProps = (state: State) => ({
|
||||
post: state.currentPost.item,
|
||||
likes: state.currentPost.likes,
|
||||
followed: state.currentPost.followed,
|
||||
comments: state.currentPost.comments,
|
||||
postStatusChanges: state.currentPost.postStatusChanges,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -24,14 +32,41 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(requestLikes(postId));
|
||||
},
|
||||
|
||||
requestFollow(postId: number) {
|
||||
dispatch(requestFollow(postId));
|
||||
},
|
||||
|
||||
requestPostStatusChanges(postId: number) {
|
||||
dispatch(requestPostStatusChanges(postId));
|
||||
},
|
||||
|
||||
changePostBoard(postId: number, newBoardId: number, authenticityToken: string) {
|
||||
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;
|
||||
|
||||
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 timeStamp = fromRailsStringToJavascriptDate(date);
|
||||
|
||||
@@ -7,13 +7,13 @@ const friendlyDate = date => {
|
||||
if (secondsPast < 60) {
|
||||
return 'just now';
|
||||
} else if (secondsPast < 3600) {
|
||||
let minutesPast = parseInt(secondsPast / 60);
|
||||
let minutesPast = Math.round(secondsPast / 60);
|
||||
return minutesPast + ' ' + (minutesPast === 1 ? 'minute' : 'minutes') + ' ago';
|
||||
} else if (secondsPast <= 86400) {
|
||||
let hoursPast = parseInt(secondsPast / 3600);
|
||||
let hoursPast = Math.round(secondsPast / 3600);
|
||||
return hoursPast + ' ' + (hoursPast === 1 ? 'hour' : 'hours') + ' ago';
|
||||
} else {
|
||||
let daysPast = parseInt(secondsPast / 86400);
|
||||
let daysPast = Math.round(secondsPast / 86400);
|
||||
return daysPast + ' ' + (daysPast === 1 ? 'day' : 'days') + ' ago';
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,13 @@ export default friendlyDate;
|
||||
Converts the default Rails datetime string
|
||||
format to a JavaScript Date object.
|
||||
*/
|
||||
const fromRailsStringToJavascriptDate = date => {
|
||||
export const fromRailsStringToJavascriptDate = date => {
|
||||
let dateOnly = date.slice(0, 10);
|
||||
let timeOnly = date.slice(11, 19);
|
||||
|
||||
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,
|
||||
TOGGLE_COMMENT_REPLY,
|
||||
SET_COMMENT_REPLY_BODY,
|
||||
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||
} from '../actions/Comment/handleCommentReplies';
|
||||
|
||||
import {
|
||||
@@ -82,6 +83,7 @@ const commentsReducer = (
|
||||
|
||||
case TOGGLE_COMMENT_REPLY:
|
||||
case SET_COMMENT_REPLY_BODY:
|
||||
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||
return {
|
||||
...state,
|
||||
replyForms: replyFormsReducer(state.replyForms, action),
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
HandleCommentRepliesType,
|
||||
TOGGLE_COMMENT_REPLY,
|
||||
SET_COMMENT_REPLY_BODY,
|
||||
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||
} from '../actions/Comment/handleCommentReplies';
|
||||
|
||||
import {
|
||||
@@ -52,21 +53,40 @@ import {
|
||||
TOGGLE_COMMENT_IS_UPDATE_SUCCESS,
|
||||
} 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 likesReducer from './likesReducer';
|
||||
import commentsReducer from './commentsReducer';
|
||||
|
||||
import { LikesState } from './likesReducer';
|
||||
import { CommentsState } from './commentsReducer';
|
||||
import postStatusChangesReducer, { PostStatusChangesState } from './postStatusChangesReducer';
|
||||
|
||||
import IPost from '../interfaces/IPost';
|
||||
|
||||
|
||||
interface CurrentPostState {
|
||||
item: IPost;
|
||||
isLoading: boolean;
|
||||
error: string;
|
||||
likes: LikesState;
|
||||
followed: boolean;
|
||||
comments: CommentsState;
|
||||
postStatusChanges: PostStatusChangesState,
|
||||
}
|
||||
|
||||
const initialState: CurrentPostState = {
|
||||
@@ -74,7 +94,9 @@ const initialState: CurrentPostState = {
|
||||
isLoading: false,
|
||||
error: '',
|
||||
likes: likesReducer(undefined, {} as LikesRequestActionTypes),
|
||||
followed: false,
|
||||
comments: commentsReducer(undefined, {} as CommentsRequestActionTypes),
|
||||
postStatusChanges: postStatusChangesReducer(undefined, {} as PostStatusChangesRequestActionTypes),
|
||||
};
|
||||
|
||||
const currentPostReducer = (
|
||||
@@ -88,7 +110,11 @@ const currentPostReducer = (
|
||||
CommentsRequestActionTypes |
|
||||
HandleCommentRepliesType |
|
||||
CommentSubmitActionTypes |
|
||||
ToggleIsUpdateSuccessAction
|
||||
ToggleIsUpdateSuccessAction |
|
||||
FollowActionTypes |
|
||||
FollowRequestActionTypes |
|
||||
PostStatusChangesRequestActionTypes |
|
||||
PostStatusChangeSubmitted
|
||||
): CurrentPostState => {
|
||||
switch (action.type) {
|
||||
case POST_REQUEST_START:
|
||||
@@ -137,11 +163,33 @@ const currentPostReducer = (
|
||||
case COMMENT_SUBMIT_SUCCESS:
|
||||
case COMMENT_SUBMIT_FAILURE:
|
||||
case TOGGLE_COMMENT_IS_UPDATE_SUCCESS:
|
||||
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||
return {
|
||||
...state,
|
||||
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:
|
||||
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,
|
||||
TOGGLE_COMMENT_REPLY,
|
||||
SET_COMMENT_REPLY_BODY,
|
||||
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||
} from '../actions/Comment/handleCommentReplies';
|
||||
|
||||
import {
|
||||
@@ -20,6 +21,7 @@ export interface ReplyFormState {
|
||||
commentId: number;
|
||||
isOpen: boolean;
|
||||
body: string;
|
||||
isPostUpdate: boolean;
|
||||
isSubmitting: boolean;
|
||||
error: string;
|
||||
}
|
||||
@@ -28,6 +30,7 @@ const initialState: ReplyFormState = {
|
||||
commentId: undefined,
|
||||
isOpen: false,
|
||||
body: '',
|
||||
isPostUpdate: false,
|
||||
isSubmitting: false,
|
||||
error: '',
|
||||
}
|
||||
@@ -58,6 +61,12 @@ const replyFormReducer = (
|
||||
body: action.body,
|
||||
};
|
||||
|
||||
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||
return {
|
||||
...state,
|
||||
isPostUpdate: !state.isPostUpdate,
|
||||
};
|
||||
|
||||
case COMMENT_SUBMIT_START:
|
||||
return {
|
||||
...state,
|
||||
@@ -69,6 +78,7 @@ const replyFormReducer = (
|
||||
...state,
|
||||
isOpen: false,
|
||||
body: '',
|
||||
isPostUpdate: false,
|
||||
isSubmitting: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
HandleCommentRepliesType,
|
||||
TOGGLE_COMMENT_REPLY,
|
||||
SET_COMMENT_REPLY_BODY,
|
||||
TOGGLE_COMMENT_IS_POST_UPDATE_FLAG,
|
||||
} from '../actions/Comment/handleCommentReplies';
|
||||
|
||||
import {
|
||||
@@ -42,6 +43,7 @@ const ReplyFormsReducer = (
|
||||
|
||||
case TOGGLE_COMMENT_REPLY:
|
||||
case SET_COMMENT_REPLY_BODY:
|
||||
case TOGGLE_COMMENT_IS_POST_UPDATE_FLAG:
|
||||
return (
|
||||
state.map(
|
||||
replyForm => (
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
.commentsContainer {
|
||||
@extend .my-3;
|
||||
|
||||
.newCommentForm {
|
||||
@extend
|
||||
.d-flex,
|
||||
.flex-column,
|
||||
.my-3;
|
||||
|
||||
.commentBodyForm {
|
||||
@extend .d-flex;
|
||||
}
|
||||
|
||||
.commentIsUpdateForm {
|
||||
@extend
|
||||
.d-flex,
|
||||
.justify-content-between,
|
||||
.mt-3;
|
||||
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
.currentUserAvatar {
|
||||
@extend
|
||||
.gravatar,
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
@extend .sidebarCard;
|
||||
|
||||
.postUpdateList {
|
||||
@extend .w-100;
|
||||
@extend
|
||||
.scroll-shadows,
|
||||
.w-100;
|
||||
|
||||
max-height: 250px;
|
||||
overflow-y: scroll;
|
||||
@@ -26,8 +28,16 @@
|
||||
@extend
|
||||
.d-flex,
|
||||
.flex-column,
|
||||
.p-2,
|
||||
.my-1;
|
||||
.p-2;
|
||||
|
||||
&: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 {
|
||||
@extend .d-flex;
|
||||
@@ -42,7 +52,7 @@
|
||||
}
|
||||
|
||||
.postUpdateListItemBody {
|
||||
@extend .m-0;
|
||||
@extend .my-1;
|
||||
|
||||
font-size: 15px;
|
||||
}
|
||||
@@ -54,7 +64,9 @@
|
||||
@extend .sidebarCard;
|
||||
|
||||
.likeList {
|
||||
@extend .w-100;
|
||||
@extend
|
||||
.scroll-shadows,
|
||||
.w-100;
|
||||
|
||||
max-height: 170px;
|
||||
overflow-y: scroll;
|
||||
@@ -69,7 +81,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actionBoxContainer {
|
||||
@extend
|
||||
.sidebarCard,
|
||||
.text-center;
|
||||
|
||||
.btn {
|
||||
@extend
|
||||
.mt-3,
|
||||
.mb-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.postAndCommentsContainer {
|
||||
|
||||
@@ -72,7 +72,7 @@ a {
|
||||
.mb-3,
|
||||
.p-2;
|
||||
|
||||
width: 250px;
|
||||
width: 280px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
color: $muted-text-color;
|
||||
}
|
||||
|
||||
.smallMutedText {
|
||||
@extend
|
||||
.mutedText,
|
||||
.m-0;
|
||||
|
||||
font-size: smaller;
|
||||
line-height: 95%;
|
||||
}
|
||||
|
||||
.uppercaseText {
|
||||
@extend
|
||||
.text-secondary,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
.container {
|
||||
max-width: 920px;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.turbolinks-progress-bar {
|
||||
@@ -34,4 +34,41 @@
|
||||
color: #fff;
|
||||
background-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;
|
||||
}
|
||||
Reference in New Issue
Block a user