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:
Riccardo Graziosi
2022-05-28 11:03:36 +02:00
committed by GitHub
parent ce7be1b30c
commit dad382d2b1
59 changed files with 1080 additions and 71 deletions

View File

@@ -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}
/>
:

View File

@@ -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;

View File

@@ -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 &bull; {comments.length} comments
activity &bull; {comments.length} comment{comments.length === 1 ? '' : 's'}
</div>
<CommentList

View File

@@ -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>

View File

@@ -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}
/>
&nbsp;
<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;

View 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;

View File

@@ -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>

View File

@@ -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>&nbsp;
<PostStatusLabel
{...postStatuses.find(postStatus => postStatus.id === postUpdate.postStatusId)}
/>
</React.Fragment>
}
</p>
<MutedText>{friendlyDate(postUpdate.updatedAt)}</MutedText>
</div>

View File

@@ -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}
/>

View File

@@ -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>
);

View File

@@ -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>
);