mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 19:17:09 +08:00
[CORL-666] Viewer Events (#2681)
* feat: viewer event system * feat: more events * feat: MORE events * fix: tests * fix: rte focus events * chore: add comments * fix: remove listening to events * chore: update RTE * fix: tests * feature: generate event docs * fix: remove obsolete line in docs * chore: improve docs * chore: improve formatting * feature: protect events.md from getting out of sync * chore: small improvements * fix: removing redundant lambda
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
## Viewer Events
|
||||
_Viewer Events_ are emitted when the viewer performs certain actions.
|
||||
They can be subscribed to using the `events` parameter in
|
||||
`Coral.createStreamEmbed`.
|
||||
```html
|
||||
<script>
|
||||
const CoralStreamEmbed = Coral.createStreamEmbed({
|
||||
events: function(events) {
|
||||
events.onAny(function(eventName, data) {
|
||||
console.log(eventName, data);
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Example events:
|
||||
- `setMainTab {tab: "PROFILE"}`
|
||||
- `showFeaturedCommentTooltip`
|
||||
- `viewConversation {from: "FEATURED_COMMENTS", commentID: "c45fb5f5-03f9-49a3-a755-488c698ca0df"}`
|
||||
|
||||
### Viewer Network Events
|
||||
|
||||
_Viewer Network Events_ are events that involves a network request and thus can succeed or fail. Succeeding events will have a `.success` appended to the event name while failing events have an `.error` appended to the event name.
|
||||
|
||||
Moreover _Viewer Network Events_ contains the `rtt` field which indicates the time it needed from initiating the request until the _UI_ has been updated with the response data.
|
||||
|
||||
Example events:
|
||||
```
|
||||
createComment.success
|
||||
{
|
||||
body: "Hello world!",
|
||||
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
|
||||
rtt: 307,
|
||||
success: {
|
||||
id: "6fecfb11-4d0f-4edc-89b7-878a9928addd"
|
||||
status: "APPROVED"`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
createComment.error
|
||||
{
|
||||
body: "Hi!",
|
||||
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
|
||||
rtt: 229,
|
||||
error: {
|
||||
code: "COMMENT_BODY_TOO_SHORT"
|
||||
message: "Comment body must have at least 10 characters."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event List
|
||||
<!-- START docs:events -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
|
||||
### Index
|
||||
- <a href="#approveComment">approveComment</a>
|
||||
- <a href="#banUser">banUser</a>
|
||||
- <a href="#cancelAccountDeletion">cancelAccountDeletion</a>
|
||||
- <a href="#changeEmail">changeEmail</a>
|
||||
- <a href="#changePassword">changePassword</a>
|
||||
- <a href="#changeUsername">changeUsername</a>
|
||||
- <a href="#closeStory">closeStory</a>
|
||||
- <a href="#copyPermalink">copyPermalink</a>
|
||||
- <a href="#createComment">createComment</a>
|
||||
- <a href="#createCommentFocus">createCommentFocus</a>
|
||||
- <a href="#createCommentReaction">createCommentReaction</a>
|
||||
- <a href="#createCommentReply">createCommentReply</a>
|
||||
- <a href="#editComment">editComment</a>
|
||||
- <a href="#featureComment">featureComment</a>
|
||||
- <a href="#gotoModeration">gotoModeration</a>
|
||||
- <a href="#ignoreUser">ignoreUser</a>
|
||||
- <a href="#loadMoreAllComments">loadMoreAllComments</a>
|
||||
- <a href="#loadMoreFeaturedComments">loadMoreFeaturedComments</a>
|
||||
- <a href="#loadMoreHistoryComments">loadMoreHistoryComments</a>
|
||||
- <a href="#loginPrompt">loginPrompt</a>
|
||||
- <a href="#openSortMenu">openSortMenu</a>
|
||||
- <a href="#openStory">openStory</a>
|
||||
- <a href="#rejectComment">rejectComment</a>
|
||||
- <a href="#removeCommentReaction">removeCommentReaction</a>
|
||||
- <a href="#removeUserIgnore">removeUserIgnore</a>
|
||||
- <a href="#replyCommentFocus">replyCommentFocus</a>
|
||||
- <a href="#reportComment">reportComment</a>
|
||||
- <a href="#requestAccountDeletion">requestAccountDeletion</a>
|
||||
- <a href="#requestDownloadCommentHistory">requestDownloadCommentHistory</a>
|
||||
- <a href="#resendEmailVerification">resendEmailVerification</a>
|
||||
- <a href="#setCommentsOrderBy">setCommentsOrderBy</a>
|
||||
- <a href="#setCommentsTab">setCommentsTab</a>
|
||||
- <a href="#setMainTab">setMainTab</a>
|
||||
- <a href="#setProfileTab">setProfileTab</a>
|
||||
- <a href="#showAbsoluteTimestamp">showAbsoluteTimestamp</a>
|
||||
- <a href="#showAllReplies">showAllReplies</a>
|
||||
- <a href="#showAuthPopup">showAuthPopup</a>
|
||||
- <a href="#showEditEmailDialog">showEditEmailDialog</a>
|
||||
- <a href="#showEditForm">showEditForm</a>
|
||||
- <a href="#showEditPasswordDialog">showEditPasswordDialog</a>
|
||||
- <a href="#showEditUsernameDialog">showEditUsernameDialog</a>
|
||||
- <a href="#showFeaturedCommentTooltip">showFeaturedCommentTooltip</a>
|
||||
- <a href="#showIgnoreUserdDialog">showIgnoreUserdDialog</a>
|
||||
- <a href="#showModerationPopover">showModerationPopover</a>
|
||||
- <a href="#showMoreOfConversation">showMoreOfConversation</a>
|
||||
- <a href="#showMoreReplies">showMoreReplies</a>
|
||||
- <a href="#showReplyForm">showReplyForm</a>
|
||||
- <a href="#showReportPopover">showReportPopover</a>
|
||||
- <a href="#showSharePopover">showSharePopover</a>
|
||||
- <a href="#showUserPopover">showUserPopover</a>
|
||||
- <a href="#signOut">signOut</a>
|
||||
- <a href="#unfeatureComment">unfeatureComment</a>
|
||||
- <a href="#updateNotificationSettings">updateNotificationSettings</a>
|
||||
- <a href="#updateStorySettings">updateStorySettings</a>
|
||||
- <a href="#viewConversation">viewConversation</a>
|
||||
- <a href="#viewFullDiscussion">viewFullDiscussion</a>
|
||||
- <a href="#viewNewComments">viewNewComments</a>
|
||||
|
||||
### Events
|
||||
- <a id="approveComment">**approveComment.success**, **approveComment.error**</a>: This event is emitted when the viewer approves a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="banUser">**banUser.success**, **banUser.error**</a>: This event is emitted when the viewer bans a user.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="cancelAccountDeletion">**cancelAccountDeletion.success**, **cancelAccountDeletion.error**</a>: This event is emitted when the viewer cancels the account deletion.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changeEmail">**changeEmail.success**, **changeEmail.error**</a>: This event is emitted when the viewer changes its email.
|
||||
```ts
|
||||
{
|
||||
oldEmail: string;
|
||||
newEmail: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changePassword">**changePassword.success**, **changePassword.error**</a>: This event is emitted when the viewer changes its password.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="changeUsername">**changeUsername.success**, **changeUsername.error**</a>: This event is emitted when the viewer changes its username.
|
||||
```ts
|
||||
{
|
||||
oldUsername: string;
|
||||
newUsername: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="closeStory">**closeStory.success**, **closeStory.error**</a>: This event is emitted when the viewer closes the story.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="copyPermalink">**copyPermalink**</a>: This event is emitted when the viewer copies the permalink with the button.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="createComment">**createComment.success**, **createComment.error**</a>: This event is emitted when a top level comment is created.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
body: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="createCommentFocus">**createCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to create a comment.
|
||||
- <a id="createCommentReaction">**createCommentReaction.success**, **createCommentReaction.error**</a>: This event is emitted when the viewer reacts to a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="createCommentReply">**createCommentReply.success**, **createCommentReply.error**</a>: This event is emitted when a comment reply is created.
|
||||
```ts
|
||||
{
|
||||
body: string;
|
||||
parentID: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="editComment">**editComment.success**, **editComment.error**</a>: This event is emitted when the viewer edits a comment.
|
||||
```ts
|
||||
{
|
||||
body: string;
|
||||
commentID: string;
|
||||
success: {
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="featureComment">**featureComment.success**, **featureComment.error**</a>: This event is emitted when the viewer features a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="gotoModeration">**gotoModeration**</a>: This event is emitted when the viewer goes to moderation.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="ignoreUser">**ignoreUser.success**, **ignoreUser.error**</a>: This event is emitted when the viewer ignores a user.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreAllComments">**loadMoreAllComments.success**, **loadMoreAllComments.error**</a>: This event is emitted when the viewer loads more top level comments into the comment stream.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreFeaturedComments">**loadMoreFeaturedComments.success**, **loadMoreFeaturedComments.error**</a>: This event is emitted when the viewer loads more featured comments.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loadMoreHistoryComments">**loadMoreHistoryComments.success**, **loadMoreHistoryComments.error**</a>: This event is emitted when the viewer loads more top level comments into the history comment stream.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="loginPrompt">**loginPrompt**</a>: This event is emitted when the viewer does an action that will prompt a login dialog.
|
||||
- <a id="openSortMenu">**openSortMenu**</a>: This event is emitted when the viewer clicks on the sort menu.
|
||||
- <a id="openStory">**openStory.success**, **openStory.error**</a>: This event is emitted when the viewer opens the story.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="rejectComment">**rejectComment.success**, **rejectComment.error**</a>: This event is emitted when the viewer rejects a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="removeCommentReaction">**removeCommentReaction.success**, **removeCommentReaction.error**</a>: This event is emitted when the viewer removes its reaction from a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="removeUserIgnore">**removeUserIgnore.success**, **removeUserIgnore.error**</a>: This event is emitted when the viewer remove a user from its ignored users list.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="replyCommentFocus">**replyCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to reply to a comment.
|
||||
- <a id="reportComment">**reportComment.success**, **reportComment.error**</a>: This event is emitted when the viewer reports a comment.
|
||||
```ts
|
||||
{
|
||||
reason: string;
|
||||
commentID: string;
|
||||
additionalDetails?: string | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="requestAccountDeletion">**requestAccountDeletion.success**, **requestAccountDeletion.error**</a>: This event is emitted when the viewer requests to delete its account.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="requestDownloadCommentHistory">**requestDownloadCommentHistory.success**, **requestDownloadCommentHistory.error**</a>: This event is emitted when the viewer requests to download its comment history.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="resendEmailVerification">**resendEmailVerification.success**, **resendEmailVerification.error**</a>: This event is emitted when the viewer request another email verification email.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="setCommentsOrderBy">**setCommentsOrderBy**</a>: This event is emitted when the viewer changes the sort order of the comments.
|
||||
```ts
|
||||
{
|
||||
orderBy: string;
|
||||
}
|
||||
```
|
||||
- <a id="setCommentsTab">**setCommentsTab**</a>: This event is emitted when the viewer changes the tab of the comments tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="setMainTab">**setMainTab**</a>: This event is emitted when the viewer changes the tab of the main tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="setProfileTab">**setProfileTab**</a>: This event is emitted when the viewer changes the tab of the profile tab bar.
|
||||
```ts
|
||||
{
|
||||
tab: string;
|
||||
}
|
||||
```
|
||||
- <a id="showAbsoluteTimestamp">**showAbsoluteTimestamp**</a>: This event is emitted when the viewer clicks on the relative timestamp to show the absolute time.
|
||||
- <a id="showAllReplies">**showAllReplies.success**, **showAllReplies.error**</a>: This event is emitted when the viewer reveals all replies of a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="showAuthPopup">**showAuthPopup**</a>: This event is emitted when the viewer requests the auth popup.
|
||||
```ts
|
||||
{
|
||||
view: string;
|
||||
}
|
||||
```
|
||||
- <a id="showEditEmailDialog">**showEditEmailDialog**</a>: This event is emitted when the viewer opens the edit email dialog.
|
||||
- <a id="showEditForm">**showEditForm**</a>: This event is emitted when the viewer opens the edit form.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showEditPasswordDialog">**showEditPasswordDialog**</a>: This event is emitted when the viewer opens the edit password dialog.
|
||||
- <a id="showEditUsernameDialog">**showEditUsernameDialog**</a>: This event is emitted when the viewer opens the edit username dialog.
|
||||
- <a id="showFeaturedCommentTooltip">**showFeaturedCommentTooltip**</a>: This event is emitted when the viewer clicks to show the featured comment tooltip.
|
||||
- <a id="showIgnoreUserdDialog">**showIgnoreUserdDialog**</a>: This event is emitted when the viewer opens the ignore user dialog.
|
||||
- <a id="showModerationPopover">**showModerationPopover**</a>: This event is emitted when the viewer opens the moderation popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showMoreOfConversation">**showMoreOfConversation.success**, **showMoreOfConversation.error**</a>: This event is emitted when the viewer reveals more of the parent conversation thread.
|
||||
```ts
|
||||
{
|
||||
commentID: string | null;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="showMoreReplies">**showMoreReplies**</a>: This event is emitted when the viewer reveals new live replies.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
count: number;
|
||||
}
|
||||
```
|
||||
- <a id="showReplyForm">**showReplyForm**</a>: This event is emitted when the viewer opens the reply form.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showReportPopover">**showReportPopover**</a>: This event is emitted when the viewer opens the report popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showSharePopover">**showSharePopover**</a>: This event is emitted when the viewer opens the share popover.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="showUserPopover">**showUserPopover**</a>: This event is emitted when the viewer clicks on a username which shows the user popover.
|
||||
```ts
|
||||
{
|
||||
userID: string;
|
||||
}
|
||||
```
|
||||
- <a id="signOut">**signOut.success**, **signOut.error**</a>: This event is emitted when the viewer signs out.
|
||||
```ts
|
||||
{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="unfeatureComment">**unfeatureComment.success**, **unfeatureComment.error**</a>: This event is emitted when the viewer unfeatures a comment.
|
||||
```ts
|
||||
{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="updateNotificationSettings">**updateNotificationSettings.success**, **updateNotificationSettings.error**</a>: This event is emitted when the viewer updates its notification settings.
|
||||
```ts
|
||||
{
|
||||
onReply?: boolean | null | undefined;
|
||||
onFeatured?: boolean | null | undefined;
|
||||
onStaffReplies?: boolean | null | undefined;
|
||||
onModeration?: boolean | null | undefined;
|
||||
digestFrequency?: "NONE" | "DAILY" | "HOURLY" | null | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="updateStorySettings">**updateStorySettings.success**, **updateStorySettings.error**</a>: This event is emitted when the viewer updates the story settings.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
live?: {
|
||||
enabled?: boolean | null | undefined;
|
||||
} | null | undefined;
|
||||
moderation?: "POST" | "PRE" | null | undefined;
|
||||
premodLinksEnable?: boolean | null | undefined;
|
||||
messageBox?: {
|
||||
enabled?: boolean | null | undefined;
|
||||
icon?: string | null | undefined;
|
||||
content?: string | null | undefined;
|
||||
} | null | undefined;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string | undefined;
|
||||
};
|
||||
}
|
||||
```
|
||||
- <a id="viewConversation">**viewConversation**</a>: This event is emitted when the viewer changes to the single conversation view.
|
||||
```ts
|
||||
{
|
||||
from: "FEATURED_COMMENTS" | "COMMENT_STREAM" | "COMMENT_HISTORY";
|
||||
commentID: string;
|
||||
}
|
||||
```
|
||||
- <a id="viewFullDiscussion">**viewFullDiscussion**</a>: This event is emitted when the viewer exits the single conversation.
|
||||
```ts
|
||||
{
|
||||
commentID: string | null;
|
||||
}
|
||||
```
|
||||
- <a id="viewNewComments">**viewNewComments**</a>: This event is emitted when the viewer reveals new live comments.
|
||||
```ts
|
||||
{
|
||||
storyID: string;
|
||||
count: number;
|
||||
}
|
||||
```
|
||||
<!-- END docs:events -->
|
||||
Generated
+3
-3
@@ -2194,9 +2194,9 @@
|
||||
}
|
||||
},
|
||||
"@coralproject/rte": {
|
||||
"version": "0.10.15",
|
||||
"resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-0.10.15.tgz",
|
||||
"integrity": "sha512-w8UWmjZxEQIW1zTMAchsmy1lzklqH2EjoyDqr9ZBed0GN6gfWfU1duTDQKc7K2igdGNRTyYfHbfXhKRIdOC6oA==",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@coralproject/rte/-/rte-0.11.0.tgz",
|
||||
"integrity": "sha512-c/m2pdxIb2lyDicX5U2l3uFoUFYuetZpxVaPVzWhTclWszDGEolYzMexKnUW6W5cqabjly4yVZYx/5SNA0vW/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bowser": "^1.0.0",
|
||||
|
||||
+5
-1
@@ -26,6 +26,7 @@
|
||||
"build:server": "gulp server",
|
||||
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
|
||||
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
|
||||
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./events.md",
|
||||
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
"generate:css-types": "tcm src/core/client/",
|
||||
@@ -147,7 +148,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.3.3",
|
||||
"@coralproject/npm-run-all": "^4.1.5",
|
||||
"@coralproject/rte": "^0.10.15",
|
||||
"@coralproject/rte": "^0.11.0",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/agent-base": "^4.2.0",
|
||||
"@types/archiver": "^3.0.0",
|
||||
@@ -381,6 +382,9 @@
|
||||
],
|
||||
"src/core/server/graph/tenant/schema/schema.graphql": [
|
||||
"graphql-schema-linter"
|
||||
],
|
||||
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,events.md}": [
|
||||
"npm run docs:events -- --verify"
|
||||
]
|
||||
},
|
||||
"bundlesize": [
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
import { codeBlock, stripIndent } from "common-tags";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import ts from "typescript";
|
||||
|
||||
interface DocEntry {
|
||||
name: string;
|
||||
docs?: string;
|
||||
type: "ViewerNetworkEvent" | "ViewerEvent";
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* We use this regexp to find a previous block that we
|
||||
* are going to update in the readme file.
|
||||
*/
|
||||
const BLOCK_REGEXP = /<!-- START docs:events -->(.|\n)*<!-- END docs:events -->/gm;
|
||||
|
||||
/** Build flags that affects AST generation */
|
||||
const buildFlags =
|
||||
// Do not truncate output.
|
||||
ts.NodeBuilderFlags.NoTruncation |
|
||||
// Use multiline object literals format.
|
||||
ts.NodeBuilderFlags.MultilineObjectLiterals;
|
||||
|
||||
/** Generate documentation for all classes in a set of .ts files */
|
||||
function gatherEntries(
|
||||
fileNames: string[],
|
||||
options: ts.CompilerOptions
|
||||
): DocEntry[] {
|
||||
// Build a program using the set of root file names in fileNames
|
||||
const program = ts.createProgram(fileNames, options);
|
||||
|
||||
const printer = ts.createPrinter({
|
||||
noEmitHelpers: true,
|
||||
omitTrailingSemicolon: true,
|
||||
removeComments: false,
|
||||
});
|
||||
|
||||
// Get the checker, we will use it to find more about classes
|
||||
const checker = program.getTypeChecker();
|
||||
|
||||
const data: DocEntry[] = [];
|
||||
|
||||
/** Hold a pointer to the sourcefile we are currently processing. */
|
||||
let currentSourceFile: ts.SourceFile;
|
||||
|
||||
// Visit every sourceFile in the program
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (!sourceFile.isDeclarationFile) {
|
||||
currentSourceFile = sourceFile;
|
||||
// Walk the tree to search for classes
|
||||
ts.forEachChild(sourceFile, visit);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = data.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
if (b.name > a.name) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
|
||||
/** visit nodes finding exported events */
|
||||
function visit(node: ts.Node) {
|
||||
// Only consider exported nodes
|
||||
if (!isNodeExported(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ts.isVariableStatement(node)) {
|
||||
if (
|
||||
!node.getFullText().includes("createViewerNetworkEvent") &&
|
||||
!node.getFullText().includes("createViewerEvent")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const firstChild = node.declarationList.declarations[0];
|
||||
if (ts.isVariableDeclaration(firstChild)) {
|
||||
const symbol = checker.getSymbolAtLocation(firstChild.name);
|
||||
if (symbol) {
|
||||
serializeEventSymbol(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function serializeEventSymbol(symbol: ts.Symbol) {
|
||||
const type = checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
symbol.valueDeclaration
|
||||
);
|
||||
const typeNode = checker.typeToTypeNode(type, undefined, buildFlags)!;
|
||||
const typeName = symbol.getName();
|
||||
const entry: DocEntry = {
|
||||
name: typeName,
|
||||
docs: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
|
||||
type: type.getSymbol()!.getName() as DocEntry["type"],
|
||||
};
|
||||
typeNode.forEachChild(ch => {
|
||||
if (ts.isTypeLiteralNode(ch)) {
|
||||
const text = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
ch,
|
||||
currentSourceFile
|
||||
);
|
||||
if (text !== "{}") {
|
||||
entry.text = text;
|
||||
}
|
||||
/*
|
||||
Go through each parameter.
|
||||
ch.members.forEach(m => {
|
||||
if (ts.isPropertySignature(m)) {
|
||||
if (ts.isIdentifier(m.name)) {
|
||||
data.parameters[m.name.text] = printer.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
m.type!,
|
||||
currentSourceFile
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
});
|
||||
data.push(entry);
|
||||
}
|
||||
|
||||
/** True if this is visible outside this file, false otherwise */
|
||||
function isNodeExported(node: ts.Node): boolean {
|
||||
return (
|
||||
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-unnecessary-type-assertion
|
||||
(ts.getCombinedModifierFlags(node as ts.Declaration) &
|
||||
ts.ModifierFlags.Export) !==
|
||||
0 ||
|
||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function prefixLines(text: string, prefix: string) {
|
||||
return text.split("\n").join(`\n${prefix}`);
|
||||
}
|
||||
|
||||
function getEventName(typeName: string) {
|
||||
return (
|
||||
typeName[0].toLocaleLowerCase() +
|
||||
typeName.slice(1, typeName.length - "Event".length)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes "%future added value" from text. This is a placeholder type
|
||||
* added by Relay to help with future proofness.
|
||||
*/
|
||||
function removeFutureAddedValue(text: string) {
|
||||
return text
|
||||
.replace(': "%future added value" | ', ": ")
|
||||
.replace(' | "%future added value"', "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Append or update previous documention in markdownFile.
|
||||
*
|
||||
* @param markdownFile The markdown file we want to inject the docs too.
|
||||
* @param entries data as returned by gatherEntries.
|
||||
*/
|
||||
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
|
||||
const previousContent = fs.existsSync(markdownFile)
|
||||
? fs.readFileSync(markdownFile).toString()
|
||||
: "";
|
||||
const summary = stripIndent`
|
||||
- ${entries
|
||||
.map(
|
||||
e => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
|
||||
)
|
||||
.join("\n - ")}
|
||||
`;
|
||||
const list = entries
|
||||
.map(
|
||||
e =>
|
||||
codeBlock`
|
||||
- ${
|
||||
e.type === "ViewerEvent"
|
||||
? `<a id="${getEventName(e.name)}">**${getEventName(e.name)}**</a>`
|
||||
: `<a id="${getEventName(e.name)}">**${getEventName(
|
||||
e.name
|
||||
)}.success**, **${getEventName(e.name)}.error**</a>`
|
||||
}: ${e.docs ? e.docs.replace("\n", " ") : ""}
|
||||
${
|
||||
e.text
|
||||
? codeBlock`
|
||||
\`\`\`ts
|
||||
${removeFutureAddedValue(e.text)}
|
||||
\`\`\`
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const output = stripIndent`
|
||||
<!-- START docs:events -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
|
||||
### Index
|
||||
${prefixLines(summary, " ")}
|
||||
|
||||
### Events
|
||||
${prefixLines(list, " ")}
|
||||
<!-- END docs:events -->
|
||||
`;
|
||||
|
||||
let newContent;
|
||||
// Find previous block.
|
||||
if (BLOCK_REGEXP.test(previousContent)) {
|
||||
newContent = previousContent.replace(BLOCK_REGEXP, output);
|
||||
} else {
|
||||
newContent = previousContent + "\n" + output;
|
||||
}
|
||||
if (previousContent === newContent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${markdownFile} is up to date`);
|
||||
return;
|
||||
}
|
||||
if (verify) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${markdownFile} is outdated, please run \`npm run docs:events\``
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(markdownFile, newContent);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Successfully injected documentation into ${markdownFile}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (process.argv.length < 4) {
|
||||
throw new Error("Must provide path to events and a markdown file.");
|
||||
}
|
||||
|
||||
const eventFile = process.argv[2];
|
||||
const markdownFile = process.argv[3];
|
||||
|
||||
// Find tsconfig file.
|
||||
const configFile = ts.findConfigFile(eventFile, fs.existsSync);
|
||||
if (!configFile) {
|
||||
throw new Error("tsconfig file not found");
|
||||
}
|
||||
const configText = fs.readFileSync(configFile).toString();
|
||||
const result = ts.parseConfigFileTextToJson(configFile, configText);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
// Parse the JSON raw data into actual consumable compiler options.
|
||||
const config = ts.parseJsonConfigFileContent(
|
||||
result.config,
|
||||
ts.sys,
|
||||
path.dirname(configFile)
|
||||
);
|
||||
|
||||
const entries = gatherEntries([eventFile], config.options);
|
||||
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,140 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { useCoralContext } from "./bootstrap";
|
||||
|
||||
/**
|
||||
* _Viewer Events_ are emitted when the viewer performs certain actions.
|
||||
* They can be subscribed to using the `events` parameter in
|
||||
* `Coral.createStreamEmbed`.
|
||||
*/
|
||||
export interface ViewerEvent<T> {
|
||||
emit: keyof T extends never
|
||||
? (eventEmitter: EventEmitter2) => void
|
||||
: (eventEmitter: EventEmitter2, data: T) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ViewerNetworkEventStarted represents ViewerNetworkEvent that has
|
||||
* started and is waiting for the response.
|
||||
*/
|
||||
export interface ViewerNetworkEventStarted<
|
||||
T extends { success: object; error: object }
|
||||
> {
|
||||
/**
|
||||
* Emits a success event and include the rtt time.
|
||||
*/
|
||||
success: keyof T["success"] extends never
|
||||
? () => void
|
||||
: (success: T["success"]) => void;
|
||||
|
||||
/**
|
||||
* Emits an error event and include the rtt time.
|
||||
*/
|
||||
error: keyof T["error"] extends never
|
||||
? () => void
|
||||
: (error: T["error"]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* _Viewer Network Events_ are _Viewer Events_ that involves a network request and
|
||||
* thus can succeed or fail. Succeeding events have the suffix `.success`
|
||||
* while failing events an `.error` suffix.
|
||||
*
|
||||
* Moreover _Viewer Network Events_ contain the `rtt` field which indicates
|
||||
* the time it needed from initiating the request until the _UI_ has been
|
||||
* updated with the response data.
|
||||
*/
|
||||
export interface ViewerNetworkEvent<
|
||||
T extends { success: object; error: object }
|
||||
> {
|
||||
/**
|
||||
* Mark the network request as started. This will also start tracking the rtt time.
|
||||
*/
|
||||
begin: keyof T extends "success" | "error"
|
||||
? (eventEmitter: EventEmitter2) => ViewerNetworkEventStarted<T>
|
||||
: (
|
||||
eventEmitter: EventEmitter2,
|
||||
data: Pick<T, Exclude<keyof T, "success" | "error">>
|
||||
) => ViewerNetworkEventStarted<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* createViewerEvent creates a ViewerNetworkEvent object.
|
||||
*
|
||||
* @param name name of the event
|
||||
*/
|
||||
export function createViewerNetworkEvent<
|
||||
T extends { success: object; error: object }
|
||||
>(name: string): ViewerNetworkEvent<T> {
|
||||
return {
|
||||
begin: ((eventEmitter, data) => {
|
||||
const ms = Date.now();
|
||||
return {
|
||||
success: (success => {
|
||||
const final: any = {
|
||||
...data,
|
||||
rtt: Date.now() - ms,
|
||||
};
|
||||
if (success) {
|
||||
final.success = success;
|
||||
}
|
||||
eventEmitter.emit(`${name}.success`, final);
|
||||
}) as ViewerNetworkEventStarted<T>["success"],
|
||||
error: (error => {
|
||||
const final: any = {
|
||||
...data,
|
||||
rtt: Date.now() - ms,
|
||||
};
|
||||
if (error) {
|
||||
final.error = error;
|
||||
}
|
||||
eventEmitter.emit(`${name}.error`, final);
|
||||
}) as ViewerNetworkEventStarted<T>["error"],
|
||||
};
|
||||
}) as ViewerNetworkEvent<T>["begin"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* createViewerEvent creates a ViewerEvent object.
|
||||
*
|
||||
* @param name name of the event
|
||||
*/
|
||||
export function createViewerEvent<T>(name: string): ViewerEvent<T> {
|
||||
return {
|
||||
emit: ((eventEmitter, data) => {
|
||||
eventEmitter.emit(name, data);
|
||||
}) as ViewerEvent<T>["emit"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useViewerEvent inject the eventEmitter and returns a simple
|
||||
* callback to emit the event.
|
||||
*/
|
||||
export function useViewerEvent<T>(
|
||||
viewerEvent: ViewerEvent<T>
|
||||
): keyof T extends never ? () => void : (data: T) => void {
|
||||
const { eventEmitter } = useCoralContext();
|
||||
return ((data?: T) => {
|
||||
viewerEvent.emit(eventEmitter, data as any);
|
||||
}) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* useViewerNetworkEvent injects the eventEmitter into a ViewNetworkEvent
|
||||
* and returns a simple callback to begin the event.
|
||||
*/
|
||||
export function useViewerNetworkEvent<
|
||||
T extends { success: object; error: object }
|
||||
>(
|
||||
viewerNetworkEvent: ViewerNetworkEvent<T>
|
||||
): keyof T extends "success" | "error"
|
||||
? () => ViewerNetworkEventStarted<T>
|
||||
: (
|
||||
data: Pick<T, Exclude<keyof T, "success" | "error">>
|
||||
) => ViewerNetworkEventStarted<T> {
|
||||
const { eventEmitter } = useCoralContext();
|
||||
return ((data?: T) => {
|
||||
return viewerNetworkEvent.begin(eventEmitter, data as any);
|
||||
}) as any;
|
||||
}
|
||||
@@ -38,7 +38,6 @@ function createMutationContainer<T extends string, I, R>(
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
this.props.context.eventEmitter.emit(`mutation.${propName}`, input);
|
||||
return commit(
|
||||
this.props.context.relayEnvironment,
|
||||
input,
|
||||
|
||||
@@ -69,7 +69,6 @@ export function useFetch<V, R>(
|
||||
const context = useCoralContext();
|
||||
return useCallback<FetchProp<typeof fetch>>(
|
||||
((variables: V) => {
|
||||
context.eventEmitter.emit(`fetch.${fetch.name}`, variables);
|
||||
return fetch.fetch(context.relayEnvironment, variables, context);
|
||||
}) as any,
|
||||
[context]
|
||||
@@ -94,10 +93,6 @@ export function withFetch<N extends string, V, R>(
|
||||
public static displayName = wrapDisplayName(BaseComponent, "withFetch");
|
||||
|
||||
private fetch = (variables: V) => {
|
||||
this.props.context.eventEmitter.emit(
|
||||
`fetch.${fetch.name}`,
|
||||
variables
|
||||
);
|
||||
return fetch.fetch(
|
||||
this.props.context.relayEnvironment,
|
||||
variables,
|
||||
|
||||
@@ -71,7 +71,6 @@ export function useMutation<I, R>(
|
||||
const context = useCoralContext();
|
||||
return useCallback<MutationProp<typeof mutation>>(
|
||||
((input: I) => {
|
||||
context.eventEmitter.emit(`mutation.${mutation.name}`, input);
|
||||
return mutation.commit(context.relayEnvironment, input, context);
|
||||
}) as any,
|
||||
[context]
|
||||
@@ -99,10 +98,6 @@ export function withMutation<N extends string, I, R>(
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
this.props.context.eventEmitter.emit(
|
||||
`mutation.${mutation.name}`,
|
||||
input
|
||||
);
|
||||
return mutation.commit(
|
||||
this.props.context.relayEnvironment,
|
||||
input,
|
||||
|
||||
@@ -50,7 +50,6 @@ export function useSubscription<V>(
|
||||
const context = useCoralContext();
|
||||
return useCallback<SubscriptionProp<typeof subscription>>(
|
||||
((variables: V) => {
|
||||
context.eventEmitter.emit(`subscription.${subscription.name}`, variables);
|
||||
return subscription.subscribe(
|
||||
context.relayEnvironment,
|
||||
variables,
|
||||
|
||||
@@ -4,23 +4,29 @@ import { RelayPaginationProp } from "react-relay";
|
||||
/**
|
||||
* useLoadMore is a react hook that returns a `loadMore` callback
|
||||
* and a `isLoadingMore` boolean.
|
||||
*
|
||||
* @param relay {RelayPaginationProp}
|
||||
* @param count {number}
|
||||
*/
|
||||
export default function useLoadMore(
|
||||
relay: RelayPaginationProp,
|
||||
count: number
|
||||
): [() => void, boolean] {
|
||||
): [() => Promise<void>, boolean] {
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const loadMore = useCallback(() => {
|
||||
if (!relay.hasMore() || relay.isLoading()) {
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
setIsLoadingMore(true);
|
||||
relay.loadMore(count, error => {
|
||||
setIsLoadingMore(false);
|
||||
if (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
relay.loadMore(count, error => {
|
||||
setIsLoadingMore(false);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [relay]);
|
||||
return [loadMore, isLoadingMore];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "coral-framework/testHelpers";
|
||||
@@ -16,6 +18,10 @@ beforeAll(() => {
|
||||
|
||||
it("Sets activeTab", () => {
|
||||
const tab = "COMMENTS";
|
||||
commit(environment, { tab });
|
||||
const eventEmitter = new EventEmitter2();
|
||||
const mock = sinon.mock(eventEmitter);
|
||||
mock.expects("emit").withArgs("setMainTab", { tab });
|
||||
commit(environment, { tab }, { eventEmitter });
|
||||
expect(source.get(LOCAL_ID)!.activeTab).toEqual(tab);
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { createMutationContainer, LOCAL_ID } from "coral-framework/lib/relay";
|
||||
import { SetMainTabEvent } from "coral-stream/events";
|
||||
|
||||
export interface SetActiveTabInput {
|
||||
tab: "COMMENTS" | "PROFILE" | "%future added value";
|
||||
@@ -10,11 +12,15 @@ export type SetActiveTabMutation = (input: SetActiveTabInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetActiveTabInput
|
||||
input: SetActiveTabInput,
|
||||
{ eventEmitter }: Pick<CoralContext, "eventEmitter">
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.tab, "activeTab");
|
||||
if (record.getValue("activeTab") !== input.tab) {
|
||||
SetMainTabEvent.emit(eventEmitter, { tab: input.tab });
|
||||
record.setValue(input.tab, "activeTab");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
@@ -28,24 +27,3 @@ it("Broadcasts events to pym", () => {
|
||||
);
|
||||
expect(pym.sendMessage.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
it("emits event aliases", () => {
|
||||
const eventEmitter: any = {
|
||||
emit: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs("loginPrompt").returns(null)
|
||||
),
|
||||
onAny: (cb: (eventName: string, value: any) => void) => {
|
||||
cb("mutation.showAuthPopup", { view: "SIGN_IN" });
|
||||
},
|
||||
};
|
||||
|
||||
const pym = {
|
||||
sendMessage: noop,
|
||||
};
|
||||
|
||||
createRenderer().render(
|
||||
<OnEvents pym={pym as any} eventEmitter={eventEmitter} />
|
||||
);
|
||||
expect(eventEmitter.emit.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Component } from "react";
|
||||
|
||||
import { CoralContext, withContext } from "coral-framework/lib/bootstrap";
|
||||
|
||||
import emitEventAliases from "./emitEventAliases";
|
||||
|
||||
interface Props {
|
||||
pym: CoralContext["pym"];
|
||||
eventEmitter: CoralContext["eventEmitter"];
|
||||
@@ -13,8 +11,6 @@ export class OnEvents extends Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
props.eventEmitter.onAny((eventName: string, value: any) => {
|
||||
// Emit event aliases.
|
||||
emitEventAliases(props.eventEmitter, eventName, value);
|
||||
props.pym!.sendMessage(
|
||||
"event",
|
||||
JSON.stringify({
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
|
||||
export default function emitEventAliases(
|
||||
eventEmitter: EventEmitter2,
|
||||
eventName: string,
|
||||
value: any
|
||||
) {
|
||||
switch (eventName) {
|
||||
case "mutation.showAuthPopup":
|
||||
switch (value.view) {
|
||||
case "SIGN_IN":
|
||||
eventEmitter.emit("loginPrompt");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { ShowAbsoluteTimestampEvent } from "coral-stream/events";
|
||||
import { Timestamp as BaseTimestamp } from "coral-ui/components";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
|
||||
const TimeStamp: FunctionComponent<
|
||||
PropTypesOf<typeof BaseTimestamp>
|
||||
> = props => {
|
||||
const emitEvent = useViewerEvent(ShowAbsoluteTimestampEvent);
|
||||
const handleOnToggle = useCallback(
|
||||
(absolute: boolean) => {
|
||||
if (absolute) {
|
||||
emitEvent();
|
||||
}
|
||||
if (props.onToggleAbsolute) {
|
||||
return props.onToggleAbsolute(absolute);
|
||||
}
|
||||
},
|
||||
[props.onToggleAbsolute, emitEvent]
|
||||
);
|
||||
return <BaseTimestamp {...props} onToggleAbsolute={handleOnToggle} />;
|
||||
};
|
||||
|
||||
export default TimeStamp;
|
||||
@@ -3,15 +3,14 @@ import React, { Component } from "react";
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import {
|
||||
graphql,
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
withMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
SignOutMutation,
|
||||
withSignOutMutation,
|
||||
} from "coral-framework/mutations";
|
||||
import {
|
||||
ShowAuthPopupMutation,
|
||||
SignOutMutation,
|
||||
withShowAuthPopupMutation,
|
||||
} from "coral-stream/mutations";
|
||||
import { Popup } from "coral-ui/components";
|
||||
@@ -33,7 +32,7 @@ interface Props {
|
||||
settings: SettingsData;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
setAuthPopupState: SetAuthPopupStateMutation;
|
||||
signOut: SignOutMutation;
|
||||
signOut: MutationProp<typeof SignOutMutation>;
|
||||
}
|
||||
|
||||
export class UserBoxContainer extends Component<Props> {
|
||||
@@ -118,7 +117,7 @@ export class UserBoxContainer extends Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSignOutMutation(
|
||||
const enhanced = withMutation(SignOutMutation)(
|
||||
withSetAuthPopupStateMutation(
|
||||
withShowAuthPopupMutation(
|
||||
withLocalStateContainer(
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
/**
|
||||
* This file contains Viewer Events of the Embed Stream.
|
||||
*
|
||||
* Viewer Events can be subscribed to using the `events` parameter in
|
||||
* `Coral.createStreamEmbed`.
|
||||
*
|
||||
* ```html
|
||||
* <script>
|
||||
* const CoralStreamEmbed = Coral.createStreamEmbed({
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
* });
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
|
||||
import {
|
||||
createViewerEvent,
|
||||
createViewerNetworkEvent,
|
||||
} from "coral-framework/lib/events";
|
||||
|
||||
import { COMMENT_STATUS } from "./__generated__/CreateCommentMutation.graphql";
|
||||
import { DIGEST_FREQUENCY } from "./__generated__/NotificationSettingsContainer_viewer.graphql";
|
||||
import { MODERATION_MODE } from "./__generated__/UpdateStorySettingsMutation.graphql";
|
||||
|
||||
/**
|
||||
* This event is emitted when a top level comment is created.
|
||||
*/
|
||||
export const CreateCommentEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
body: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("createComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when a comment reply is created.
|
||||
*/
|
||||
export const CreateCommentReplyEvent = createViewerNetworkEvent<{
|
||||
body: string;
|
||||
parentID: string;
|
||||
success: {
|
||||
id: string;
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("createCommentReply");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer edits a comment.
|
||||
*/
|
||||
export const EditCommentEvent = createViewerNetworkEvent<{
|
||||
body: string;
|
||||
commentID: string;
|
||||
success: {
|
||||
status: COMMENT_STATUS;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("editComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reacts to a comment.
|
||||
*/
|
||||
export const CreateCommentReactionEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("createCommentReaction");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer removes its reaction from a comment.
|
||||
*/
|
||||
export const RemoveCommentReactionEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("removeCommentReaction");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer features a comment.
|
||||
*/
|
||||
export const FeatureCommentEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("featureComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer unfeatures a comment.
|
||||
*/
|
||||
export const UnfeatureCommentEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("unfeatureComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer approves a comment.
|
||||
*/
|
||||
export const ApproveCommentEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("approveComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer rejects a comment.
|
||||
*/
|
||||
export const RejectCommentEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("rejectComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer bans a user.
|
||||
*/
|
||||
export const BanUserEvent = createViewerNetworkEvent<{
|
||||
userID: string;
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("banUser");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer ignores a user.
|
||||
*/
|
||||
export const IgnoreUserEvent = createViewerNetworkEvent<{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("ignoreUser");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer remove a user from
|
||||
* its ignored users list.
|
||||
*/
|
||||
export const RemoveUserIgnoreEvent = createViewerNetworkEvent<{
|
||||
userID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("removeUserIgnore");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer signs out.
|
||||
*/
|
||||
export const SignOutEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("signOut");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer updates its
|
||||
* notification settings.
|
||||
*/
|
||||
export const UpdateNotificationSettingsEvent = createViewerNetworkEvent<{
|
||||
onReply?: boolean | null;
|
||||
onFeatured?: boolean | null;
|
||||
onStaffReplies?: boolean | null;
|
||||
onModeration?: boolean | null;
|
||||
digestFrequency?: DIGEST_FREQUENCY | null;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("updateNotificationSettings");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer updates the story settings.
|
||||
*/
|
||||
export const UpdateStorySettingsEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
live?: {
|
||||
enabled?: boolean | null;
|
||||
} | null;
|
||||
moderation?: MODERATION_MODE | null;
|
||||
premodLinksEnable?: boolean | null;
|
||||
messageBox?: {
|
||||
enabled?: boolean | null;
|
||||
icon?: string | null;
|
||||
content?: string | null;
|
||||
} | null;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("updateStorySettings");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer closes the story.
|
||||
*/
|
||||
export const CloseStoryEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("closeStoryEvent");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the story.
|
||||
*/
|
||||
export const OpenStoryEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("openStoryEvent");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer loads more
|
||||
* featured comments.
|
||||
*/
|
||||
export const LoadMoreFeaturedCommentsEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: { message: string; code?: string };
|
||||
}>("loadMoreFeaturedComments");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer loads more
|
||||
* top level comments into the comment stream.
|
||||
*/
|
||||
export const LoadMoreAllCommentsEvent = createViewerNetworkEvent<{
|
||||
storyID: string;
|
||||
success: {};
|
||||
error: { message: string; code?: string };
|
||||
}>("loadMoreAllComments");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer loads more
|
||||
* top level comments into the history comment stream.
|
||||
*/
|
||||
export const LoadMoreHistoryCommentsEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: { message: string; code?: string };
|
||||
}>("loadMoreHistoryComments");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reveals
|
||||
* all replies of a comment.
|
||||
*/
|
||||
export const ShowAllRepliesEvent = createViewerNetworkEvent<{
|
||||
commentID: string;
|
||||
success: {};
|
||||
error: { message: string; code?: string };
|
||||
}>("showAllReplies");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer does an
|
||||
* action that will prompt a login dialog.
|
||||
*/
|
||||
export const LoginPromptEvent = createViewerEvent("loginPrompt");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer requests the auth popup.
|
||||
*/
|
||||
export const ShowAuthPopupEvent = createViewerEvent<{
|
||||
view: string;
|
||||
}>("showAuthPopup");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes the
|
||||
* tab of the main tab bar.
|
||||
*/
|
||||
export const SetMainTabEvent = createViewerEvent<{
|
||||
tab: string;
|
||||
}>("setMainTab");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes the
|
||||
* tab of the profile tab bar.
|
||||
*/
|
||||
export const SetProfileTabEvent = createViewerEvent<{
|
||||
tab: string;
|
||||
}>("setProfileTab");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes the
|
||||
* tab of the comments tab bar.
|
||||
*/
|
||||
export const SetCommentsTabEvent = createViewerEvent<{
|
||||
tab: string;
|
||||
}>("setCommentsTab");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes the
|
||||
* sort order of the comments.
|
||||
*/
|
||||
export const SetCommentsOrderByEvent = createViewerEvent<{
|
||||
orderBy: string;
|
||||
}>("setCommentsOrderBy");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes to
|
||||
* the single conversation view.
|
||||
*/
|
||||
export const ViewConversationEvent = createViewerEvent<{
|
||||
from: "FEATURED_COMMENTS" | "COMMENT_STREAM" | "COMMENT_HISTORY";
|
||||
commentID: string;
|
||||
}>("viewConversation");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer clicks
|
||||
* on a username which shows the user popover.
|
||||
*/
|
||||
export const ShowUserPopoverEvent = createViewerEvent<{
|
||||
userID: string;
|
||||
}>("showUserPopover");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer clicks
|
||||
* on the relative timestamp to show the absolute time.
|
||||
*/
|
||||
export const ShowAbsoluteTimestampEvent = createViewerEvent(
|
||||
"showAbsoluteTimestamp"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer clicks to show the
|
||||
* featured comment tooltip.
|
||||
*/
|
||||
export const ShowFeaturedCommentTooltipEvent = createViewerEvent(
|
||||
"showFeaturedCommentTooltip"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer clicks on the sort menu.
|
||||
*/
|
||||
export const OpenSortMenuEvent = createViewerEvent("openSortMenu");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer focus on the RTE to
|
||||
* create a comment.
|
||||
*/
|
||||
export const CreateCommentFocusEvent = createViewerEvent("createCommentFocus");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer focus on the RTE to
|
||||
* reply to a comment.
|
||||
*/
|
||||
export const ReplyCommentFocusEvent = createViewerEvent("replyCommentFocus");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer exits the single conversation.
|
||||
*/
|
||||
export const ViewFullDiscussionEvent = createViewerEvent<{
|
||||
commentID: string | null;
|
||||
}>("viewFullDiscussion");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reveals more of
|
||||
* the parent conversation thread.
|
||||
*/
|
||||
export const ShowMoreOfConversationEvent = createViewerNetworkEvent<{
|
||||
commentID: string | null;
|
||||
success: {};
|
||||
error: { message: string; code?: string };
|
||||
}>("showMoreOfConversation");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the share popover.
|
||||
*/
|
||||
export const ShowSharePopoverEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("showSharePopover");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer copies the permalink with the button.
|
||||
*/
|
||||
export const CopyPermalinkEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("copyPermalink");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the report popover.
|
||||
*/
|
||||
export const ShowReportPopoverEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("showReportPopover");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reports a comment.
|
||||
*/
|
||||
export const ReportCommentEvent = createViewerNetworkEvent<{
|
||||
reason: string;
|
||||
commentID: string;
|
||||
additionalDetails?: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("reportComment");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the reply form.
|
||||
*/
|
||||
export const ShowReplyFormEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("showReplyForm");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the edit form.
|
||||
*/
|
||||
export const ShowEditFormEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("showEditForm");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reveals
|
||||
* new live comments.
|
||||
*/
|
||||
export const ViewNewCommentsEvent = createViewerEvent<{
|
||||
storyID: string;
|
||||
count: number;
|
||||
}>("viewNewComments");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer reveals
|
||||
* new live replies.
|
||||
*/
|
||||
export const ShowMoreRepliesEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
count: number;
|
||||
}>("showMoreReplies");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens
|
||||
* the moderation popover.
|
||||
*/
|
||||
export const ShowModerationPopoverEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("showModerationPopover");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer goes to
|
||||
* moderation.
|
||||
*/
|
||||
export const GotoModerationEvent = createViewerEvent<{
|
||||
commentID: string;
|
||||
}>("gotoModeration");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the
|
||||
* edit username dialog.
|
||||
*/
|
||||
export const ShowEditUsernameDialogEvent = createViewerEvent(
|
||||
"showEditUsernameDialog"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the
|
||||
* edit email dialog.
|
||||
*/
|
||||
export const ShowEditEmailDialogEvent = createViewerEvent(
|
||||
"showEditEmailDialog"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the
|
||||
* edit password dialog.
|
||||
*/
|
||||
export const ShowEditPasswordDialogEvent = createViewerEvent(
|
||||
"showEditPasswordDialog"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer opens the
|
||||
* ignore user dialog.
|
||||
*/
|
||||
export const ShowIgnoreUserdDialogEvent = createViewerEvent(
|
||||
"showIgnoreUserdDialog"
|
||||
);
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer request another
|
||||
* email verification email.
|
||||
*/
|
||||
export const ResendEmailVerificationEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("resendEmailVerification");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes its username.
|
||||
*/
|
||||
export const ChangeUsernameEvent = createViewerNetworkEvent<{
|
||||
oldUsername: string;
|
||||
newUsername: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("changeUsername");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes its email.
|
||||
*/
|
||||
export const ChangeEmailEvent = createViewerNetworkEvent<{
|
||||
oldEmail: string;
|
||||
newEmail: string;
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("changeEmail");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer changes its password.
|
||||
*/
|
||||
export const ChangePasswordEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("changePassword");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer requests to download
|
||||
* its comment history.
|
||||
*/
|
||||
export const RequestDownloadCommentHistoryEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("requestDownloadCommentHistory");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer requests to delete
|
||||
* its account.
|
||||
*/
|
||||
export const RequestAccountDeletionEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("requestAccountDeletionEvent");
|
||||
|
||||
/**
|
||||
* This event is emitted when the viewer cancels the
|
||||
* account deletion.
|
||||
*/
|
||||
export const CancelAccountDeletionEvent = createViewerNetworkEvent<{
|
||||
success: {};
|
||||
error: {
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
}>("cancelAccountDeletionEvent");
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { CancelAccountDeletionEvent } from "coral-stream/events";
|
||||
|
||||
import { CancelAccountDeletionMutation as MutationTypes } from "coral-stream/__generated__/CancelAccountDeletionMutation.graphql";
|
||||
|
||||
@@ -14,35 +15,55 @@ let clientMutationId = 0;
|
||||
|
||||
const CancelAccountDeletionMutation = createMutation(
|
||||
"cancelAccountDeletionMutation",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation CancelAccountDeletionMutation(
|
||||
$input: CancelAccountDeletionInput!
|
||||
) {
|
||||
cancelAccountDeletion(input: $input) {
|
||||
user {
|
||||
scheduledDeletionDate
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const cancelAccountDeletionEvent = CancelAccountDeletionEvent.begin(
|
||||
eventEmitter
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation CancelAccountDeletionMutation(
|
||||
$input: CancelAccountDeletionInput!
|
||||
) {
|
||||
cancelAccountDeletion(input: $input) {
|
||||
user {
|
||||
scheduledDeletionDate
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const viewerProxy = store.get(viewer.id);
|
||||
if (viewerProxy !== null) {
|
||||
viewerProxy.setValue(null, "scheduledDeletionDate");
|
||||
}
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
|
||||
const viewerProxy = store.get(viewer.id);
|
||||
if (viewerProxy !== null) {
|
||||
viewerProxy.setValue(null, "scheduledDeletionDate");
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
cancelAccountDeletionEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
cancelAccountDeletionEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default CancelAccountDeletionMutation;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { createRelayEnvironment } from "coral-framework/testHelpers";
|
||||
|
||||
@@ -6,9 +8,10 @@ import { AUTH_POPUP_ID, AUTH_POPUP_TYPE } from "../local";
|
||||
import { commit } from "./ShowAuthPopupMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
let source: RecordSource;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
source = new RecordSource();
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
initLocalState: (localRecord, sourceProxy) => {
|
||||
@@ -20,23 +23,36 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("opens popup", () => {
|
||||
commit(environment, { view: "SIGN_IN" });
|
||||
it("emits ShowAuthPopupEvent and LoginPromptEvent on SIGN_IN", () => {
|
||||
const view = "SIGN_IN";
|
||||
const eventEmitter = new EventEmitter2();
|
||||
const mock = sinon.mock(eventEmitter);
|
||||
mock.expects("emit").withArgs("loginPrompt");
|
||||
mock.expects("emit").withArgs("showAuthPopup", { view });
|
||||
commit(environment, { view }, { eventEmitter });
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it("emits only ShowAuthPopupEvent on other views", () => {
|
||||
const view = "FORGOT_PASSWORD";
|
||||
const eventEmitter = new EventEmitter2();
|
||||
const mock = sinon.mock(eventEmitter);
|
||||
mock.expects("emit").withArgs("showAuthPopup", { view });
|
||||
commit(environment, { view }, { eventEmitter });
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it("opens popup or focus if already open", () => {
|
||||
const view = "SIGN_IN";
|
||||
const context = {
|
||||
eventEmitter: new EventEmitter2(),
|
||||
};
|
||||
commit(environment, { view }, context);
|
||||
expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true);
|
||||
expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(false);
|
||||
expect(source.get(AUTH_POPUP_ID)!.view).toEqual("SIGN_IN");
|
||||
});
|
||||
|
||||
it("focuses popup", () => {
|
||||
commit(environment, { view: "SIGN_IN" });
|
||||
commit(environment, { view }, context);
|
||||
expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true);
|
||||
expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true);
|
||||
expect(source.get(AUTH_POPUP_ID)!.view).toEqual("SIGN_IN");
|
||||
});
|
||||
|
||||
it("only change view when opened and focused", () => {
|
||||
commit(environment, { view: "FORGOT_PASSWORD" });
|
||||
expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true);
|
||||
expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true);
|
||||
expect(source.get(AUTH_POPUP_ID)!.view).toEqual("FORGOT_PASSWORD");
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
createMutation,
|
||||
createMutationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { LoginPromptEvent, ShowAuthPopupEvent } from "coral-stream/events";
|
||||
|
||||
import { AUTH_POPUP_ID } from "../local";
|
||||
|
||||
@@ -17,8 +19,13 @@ export type ShowAuthPopupMutation = (
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: ShowAuthPopupInput
|
||||
input: ShowAuthPopupInput,
|
||||
{ eventEmitter }: Pick<CoralContext, "eventEmitter">
|
||||
) {
|
||||
if (input.view === "SIGN_IN") {
|
||||
LoginPromptEvent.emit(eventEmitter);
|
||||
}
|
||||
ShowAuthPopupEvent.emit(eventEmitter, { view: input.view });
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(AUTH_POPUP_ID)!;
|
||||
record.setValue(input.view, "view");
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
import { commit as signOut } from "coral-framework/mutations/SignOutMutation";
|
||||
import { SignOutEvent } from "coral-stream/events";
|
||||
|
||||
const SignOutMutation = createMutation(
|
||||
"signOut",
|
||||
async (environment: Environment, input: undefined, ctx: CoralContext) => {
|
||||
const signOutEvent = SignOutEvent.begin(ctx.eventEmitter);
|
||||
try {
|
||||
await signOut(environment, input, ctx);
|
||||
signOutEvent.success();
|
||||
} catch (error) {
|
||||
signOutEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default SignOutMutation;
|
||||
@@ -7,3 +7,4 @@ export {
|
||||
withShowAuthPopupMutation,
|
||||
ShowAuthPopupMutation,
|
||||
} from "./ShowAuthPopupMutation";
|
||||
export { default as SignOutMutation } from "./SignOutMutation";
|
||||
|
||||
@@ -3,7 +3,8 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import HTMLContent from "coral-stream/common/HTMLContent";
|
||||
import { Flex, HorizontalGutter, Timestamp } from "coral-ui/components";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import { Flex, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import EditedMarker from "./EditedMarker";
|
||||
import InReplyTo from "./InReplyTo";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
@@ -16,6 +17,7 @@ type Props = PropTypesOf<typeof CommentContainerN>;
|
||||
function createDefaultProps(add: DeepPartial<Props> = {}): Props {
|
||||
return pureMerge(
|
||||
{
|
||||
eventEmitter: new EventEmitter2(),
|
||||
viewer: null,
|
||||
story: {
|
||||
url: "http://localhost/story",
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import cn from "classnames";
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component, MouseEvent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { isBeforeDate } from "coral-common/utils";
|
||||
import { getURLWithCommentID } from "coral-framework/helpers";
|
||||
import { withContext } from "coral-framework/lib/bootstrap";
|
||||
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
|
||||
import { GQLTAG, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import {
|
||||
ShowEditFormEvent,
|
||||
ShowReplyFormEvent,
|
||||
ViewConversationEvent,
|
||||
} from "coral-stream/events";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
ShowAuthPopupMutation,
|
||||
@@ -45,6 +52,7 @@ interface Props {
|
||||
comment: CommentData;
|
||||
story: StoryData;
|
||||
settings: SettingsData;
|
||||
eventEmitter: EventEmitter2;
|
||||
indentLevel?: number;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
@@ -111,9 +119,16 @@ export class CommentContainer extends Component<Props, State> {
|
||||
|
||||
private toggleReplyDialog = () => {
|
||||
if (this.props.viewer) {
|
||||
this.setState(state => ({
|
||||
showReplyDialog: !state.showReplyDialog,
|
||||
}));
|
||||
this.setState(state => {
|
||||
if (!state.showReplyDialog) {
|
||||
ShowReplyFormEvent.emit(this.props.eventEmitter, {
|
||||
commentID: this.props.comment.id,
|
||||
});
|
||||
}
|
||||
return {
|
||||
showReplyDialog: !state.showReplyDialog,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.props.showAuthPopup({ view: "SIGN_IN" });
|
||||
}
|
||||
@@ -121,6 +136,9 @@ export class CommentContainer extends Component<Props, State> {
|
||||
|
||||
private openEditDialog = () => {
|
||||
if (this.props.viewer) {
|
||||
ShowEditFormEvent.emit(this.props.eventEmitter, {
|
||||
commentID: this.props.comment.id,
|
||||
});
|
||||
this.setState(state => ({
|
||||
showEditDialog: true,
|
||||
}));
|
||||
@@ -151,6 +169,10 @@ export class CommentContainer extends Component<Props, State> {
|
||||
}
|
||||
|
||||
private handleShowConversation = (e: MouseEvent) => {
|
||||
ViewConversationEvent.emit(this.props.eventEmitter, {
|
||||
commentID: this.props.comment.id,
|
||||
from: "COMMENT_STREAM",
|
||||
});
|
||||
e.preventDefault();
|
||||
this.props.setCommentID({ id: this.props.comment.id });
|
||||
return false;
|
||||
@@ -368,85 +390,87 @@ export class CommentContainer extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetCommentIDMutation(
|
||||
withShowAuthPopupMutation(
|
||||
withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment CommentContainer_viewer on User {
|
||||
id
|
||||
status {
|
||||
current
|
||||
}
|
||||
ignoredUsers {
|
||||
const enhanced = withContext(({ eventEmitter }) => ({ eventEmitter }))(
|
||||
withSetCommentIDMutation(
|
||||
withShowAuthPopupMutation(
|
||||
withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment CommentContainer_viewer on User {
|
||||
id
|
||||
status {
|
||||
current
|
||||
}
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
badges
|
||||
role
|
||||
scheduledDeletionDate
|
||||
...UsernameWithPopoverContainer_viewer
|
||||
...ReactionButtonContainer_viewer
|
||||
...ReportButtonContainer_viewer
|
||||
...CaretContainer_viewer
|
||||
}
|
||||
badges
|
||||
role
|
||||
scheduledDeletionDate
|
||||
...UsernameWithPopoverContainer_viewer
|
||||
...ReactionButtonContainer_viewer
|
||||
...ReportButtonContainer_viewer
|
||||
...CaretContainer_viewer
|
||||
}
|
||||
`,
|
||||
story: graphql`
|
||||
fragment CommentContainer_story on Story {
|
||||
url
|
||||
isClosed
|
||||
...CaretContainer_story
|
||||
...ReplyCommentFormContainer_story
|
||||
...PermalinkButtonContainer_story
|
||||
...EditCommentFormContainer_story
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
author {
|
||||
...UsernameWithPopoverContainer_user
|
||||
`,
|
||||
story: graphql`
|
||||
fragment CommentContainer_story on Story {
|
||||
url
|
||||
isClosed
|
||||
...CaretContainer_story
|
||||
...ReplyCommentFormContainer_story
|
||||
...PermalinkButtonContainer_story
|
||||
...EditCommentFormContainer_story
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
username
|
||||
}
|
||||
parent {
|
||||
author {
|
||||
...UsernameWithPopoverContainer_user
|
||||
id
|
||||
username
|
||||
}
|
||||
parent {
|
||||
author {
|
||||
username
|
||||
}
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
status
|
||||
editing {
|
||||
edited
|
||||
editableUntil
|
||||
}
|
||||
tags {
|
||||
code
|
||||
}
|
||||
pending
|
||||
lastViewerAction
|
||||
deleted
|
||||
...ReplyCommentFormContainer_comment
|
||||
...EditCommentFormContainer_comment
|
||||
...ReactionButtonContainer_comment
|
||||
...ReportButtonContainer_comment
|
||||
...CaretContainer_comment
|
||||
...RejectedTombstoneContainer_comment
|
||||
...AuthorBadgesContainer_comment
|
||||
...UserTagsContainer_comment
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
status
|
||||
editing {
|
||||
edited
|
||||
editableUntil
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment CommentContainer_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReactionButtonContainer_settings
|
||||
...ReplyCommentFormContainer_settings
|
||||
...EditCommentFormContainer_settings
|
||||
...UserTagsContainer_settings
|
||||
}
|
||||
tags {
|
||||
code
|
||||
}
|
||||
pending
|
||||
lastViewerAction
|
||||
deleted
|
||||
...ReplyCommentFormContainer_comment
|
||||
...EditCommentFormContainer_comment
|
||||
...ReactionButtonContainer_comment
|
||||
...ReportButtonContainer_comment
|
||||
...CaretContainer_comment
|
||||
...RejectedTombstoneContainer_comment
|
||||
...AuthorBadgesContainer_comment
|
||||
...UserTagsContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment CommentContainer_settings on Settings {
|
||||
disableCommenting {
|
||||
enabled
|
||||
}
|
||||
...ReactionButtonContainer_settings
|
||||
...ReplyCommentFormContainer_settings
|
||||
...EditCommentFormContainer_settings
|
||||
...UserTagsContainer_settings
|
||||
}
|
||||
`,
|
||||
})(CommentContainer)
|
||||
`,
|
||||
})(CommentContainer)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "coral-framework/lib/form";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import {
|
||||
AriaInfo,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
Message,
|
||||
MessageIcon,
|
||||
RelativeTime,
|
||||
Timestamp,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../../helpers";
|
||||
|
||||
+43
-28
@@ -13,6 +13,7 @@ import {
|
||||
import { GQLComment } from "coral-framework/schema";
|
||||
|
||||
import { EditCommentMutation as MutationTypes } from "coral-stream/__generated__/EditCommentMutation.graphql";
|
||||
import { EditCommentEvent } from "coral-stream/events";
|
||||
|
||||
export type EditCommentInput = MutationInput<MutationTypes>;
|
||||
|
||||
@@ -36,39 +37,53 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: EditCommentInput,
|
||||
{ uuidGenerator }: CoralContext
|
||||
{ uuidGenerator, eventEmitter }: CoralContext
|
||||
) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "body"]),
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
editComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
body: input.body,
|
||||
status: lookup<GQLComment>(environment, input.commentID)!.status,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
editing: {
|
||||
edited: true,
|
||||
const editCommentEvent = EditCommentEvent.begin(eventEmitter, {
|
||||
body: input.body,
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "body"]),
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("EDIT", "lastViewerAction");
|
||||
},
|
||||
});
|
||||
optimisticResponse: {
|
||||
editComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
body: input.body,
|
||||
status: lookup<GQLComment>(environment, input.commentID)!.status,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
editing: {
|
||||
edited: true,
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("EDIT", "lastViewerAction");
|
||||
},
|
||||
}
|
||||
);
|
||||
editCommentEvent.success({ status: result.comment.status });
|
||||
return result;
|
||||
} catch (error) {
|
||||
editCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withEditCommentMutation = createMutationContainer(
|
||||
|
||||
+49
-29
@@ -1,12 +1,14 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
|
||||
import { ApproveCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { ApproveCommentMutation as MutationTypes } from "coral-stream/__generated__/ApproveCommentMutation.graphql";
|
||||
|
||||
@@ -14,37 +16,55 @@ let clientMutationId = 0;
|
||||
|
||||
const ApproveCommentMutation = createMutation(
|
||||
"approveComment",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation ApproveCommentMutation($input: ApproveCommentInput!) {
|
||||
approveComment(input: $input) {
|
||||
comment {
|
||||
status
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const approveCommentEvent = ApproveCommentEvent.begin(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation ApproveCommentMutation($input: ApproveCommentInput!) {
|
||||
approveComment(input: $input) {
|
||||
comment {
|
||||
status
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
optimisticResponse: {
|
||||
approveComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: GQLCOMMENT_STATUS.APPROVED,
|
||||
`,
|
||||
optimisticResponse: {
|
||||
approveComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: GQLCOMMENT_STATUS.APPROVED,
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("APPROVE", "lastViewerAction");
|
||||
},
|
||||
})
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("APPROVE", "lastViewerAction");
|
||||
},
|
||||
}
|
||||
);
|
||||
approveCommentEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
approveCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default ApproveCommentMutation;
|
||||
|
||||
+51
-36
@@ -8,6 +8,7 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS, GQLTAG } from "coral-framework/schema";
|
||||
import { FeatureCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { FeatureCommentMutation as MutationTypes } from "coral-stream/__generated__/FeatureCommentMutation.graphql";
|
||||
|
||||
@@ -30,47 +31,61 @@ function incrementCount(store: RecordSourceSelectorProxy, storyID: string) {
|
||||
|
||||
const FeatureCommentMutation = createMutation(
|
||||
"featureComment",
|
||||
(
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes> & { storyID: string },
|
||||
{ uuidGenerator }: CoralContext
|
||||
) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation FeatureCommentMutation($input: FeatureCommentInput!) {
|
||||
featureComment(input: $input) {
|
||||
comment {
|
||||
tags {
|
||||
code
|
||||
{ uuidGenerator, eventEmitter }: CoralContext
|
||||
) => {
|
||||
const featuredCommentEvent = FeatureCommentEvent.begin(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation FeatureCommentMutation($input: FeatureCommentInput!) {
|
||||
featureComment(input: $input) {
|
||||
comment {
|
||||
tags {
|
||||
code
|
||||
}
|
||||
status
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
status
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
`,
|
||||
optimisticUpdater: store => {
|
||||
const comment = store.get(input.commentID)!;
|
||||
const tags = comment.getLinkedRecords("tags");
|
||||
if (tags) {
|
||||
const newTag = store.create(uuidGenerator(), "Tag");
|
||||
newTag.setValue(GQLTAG.FEATURED, "code");
|
||||
comment.setLinkedRecords(tags.concat(newTag), "tags");
|
||||
comment.setValue(GQLCOMMENT_STATUS.APPROVED, "status");
|
||||
}
|
||||
incrementCount(store, input.storyID);
|
||||
},
|
||||
updater: store => {
|
||||
incrementCount(store, input.storyID);
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
optimisticUpdater: store => {
|
||||
const comment = store.get(input.commentID)!;
|
||||
const tags = comment.getLinkedRecords("tags");
|
||||
if (tags) {
|
||||
const newTag = store.create(uuidGenerator(), "Tag");
|
||||
newTag.setValue(GQLTAG.FEATURED, "code");
|
||||
comment.setLinkedRecords(tags.concat(newTag), "tags");
|
||||
comment.setValue(GQLCOMMENT_STATUS.APPROVED, "status");
|
||||
}
|
||||
incrementCount(store, input.storyID);
|
||||
},
|
||||
updater: store => {
|
||||
incrementCount(store, input.storyID);
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
featuredCommentEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
featuredCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default FeatureCommentMutation;
|
||||
|
||||
+8
-1
@@ -3,8 +3,10 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { GotoModerationEvent } from "coral-stream/events";
|
||||
import { DropdownButton, DropdownDivider, Icon } from "coral-ui/components";
|
||||
|
||||
import { ModerationActionsContainer_comment } from "coral-stream/__generated__/ModerationActionsContainer_comment.graphql";
|
||||
@@ -34,11 +36,16 @@ const ModerationActionsContainer: FunctionComponent<Props> = ({
|
||||
onDismiss,
|
||||
onBan,
|
||||
}) => {
|
||||
const emitGotoModerationEvent = useViewerEvent(GotoModerationEvent);
|
||||
const approve = useMutation(ApproveCommentMutation);
|
||||
const feature = useMutation(FeatureCommentMutation);
|
||||
const unfeature = useMutation(UnfeatureCommentMutation);
|
||||
const reject = useMutation(RejectCommentMutation);
|
||||
|
||||
const onGotoModerate = useCallback(() => {
|
||||
emitGotoModerationEvent({ commentID: comment.id });
|
||||
}, [emitGotoModerationEvent, comment.id]);
|
||||
|
||||
const onApprove = useCallback(() => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
@@ -52,7 +59,6 @@ const ModerationActionsContainer: FunctionComponent<Props> = ({
|
||||
await reject({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: story.id,
|
||||
});
|
||||
}, [approve, comment, story]);
|
||||
const onFeature = useCallback(() => {
|
||||
@@ -193,6 +199,7 @@ const ModerationActionsContainer: FunctionComponent<Props> = ({
|
||||
className={CLASSES.moderationDropdown.goToModerateButton}
|
||||
href={`/admin/moderate/comment/${comment.id}`}
|
||||
target="_blank"
|
||||
onClick={onGotoModerate}
|
||||
anchor
|
||||
>
|
||||
Go to Moderate
|
||||
|
||||
+14
-1
@@ -1,8 +1,15 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { ShowModerationPopoverEvent } from "coral-stream/events";
|
||||
import { Dropdown } from "coral-ui/components";
|
||||
|
||||
import { ModerationDropdownContainer_comment } from "coral-stream/__generated__/ModerationDropdownContainer_comment.graphql";
|
||||
@@ -29,12 +36,18 @@ const ModerationDropdownContainer: FunctionComponent<Props> = ({
|
||||
onDismiss,
|
||||
scheduleUpdate,
|
||||
}) => {
|
||||
const emitShowEvent = useViewerEvent(ShowModerationPopoverEvent);
|
||||
const [view, setView] = useState<View>("MODERATE");
|
||||
const onBan = useCallback(() => {
|
||||
setView("BAN");
|
||||
scheduleUpdate();
|
||||
}, [setView, scheduleUpdate]);
|
||||
|
||||
// run once.
|
||||
useEffect(() => {
|
||||
emitShowEvent({ commentID: comment.id });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{view === "MODERATE" ? (
|
||||
|
||||
+70
-44
@@ -1,12 +1,14 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
|
||||
import { RejectCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { RejectCommentMutation as MutationTypes } from "coral-stream/__generated__/RejectCommentMutation.graphql";
|
||||
|
||||
@@ -14,58 +16,82 @@ let clientMutationId = 0;
|
||||
|
||||
const RejectCommentMutation = createMutation(
|
||||
"rejectComment",
|
||||
(
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes> & { storyID: string }
|
||||
) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RejectCommentMutation($input: RejectCommentInput!) {
|
||||
rejectComment(input: $input) {
|
||||
comment {
|
||||
status
|
||||
tags {
|
||||
code
|
||||
}
|
||||
story {
|
||||
commentCounts {
|
||||
input: MutationInput<MutationTypes> & { noEmit?: boolean },
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
let rejectCommentEvent: ReturnType<
|
||||
typeof RejectCommentEvent.begin
|
||||
> | null = null;
|
||||
if (!input.noEmit) {
|
||||
rejectCommentEvent = RejectCommentEvent.begin(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation RejectCommentMutation($input: RejectCommentInput!) {
|
||||
rejectComment(input: $input) {
|
||||
comment {
|
||||
status
|
||||
tags {
|
||||
FEATURED
|
||||
code
|
||||
}
|
||||
story {
|
||||
commentCounts {
|
||||
tags {
|
||||
FEATURED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
rejectComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
story: {
|
||||
commentCounts: {
|
||||
tags: {
|
||||
FEATURED: 0,
|
||||
},
|
||||
},
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
commentRevisionID: input.commentRevisionID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("REJECT", "lastViewerAction");
|
||||
},
|
||||
})
|
||||
optimisticResponse: {
|
||||
rejectComment: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
status: GQLCOMMENT_STATUS.REJECTED,
|
||||
story: {
|
||||
commentCounts: {
|
||||
tags: {
|
||||
FEATURED: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
store.get(input.commentID)!.setValue("REJECT", "lastViewerAction");
|
||||
},
|
||||
}
|
||||
);
|
||||
if (rejectCommentEvent) {
|
||||
rejectCommentEvent.success();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (rejectCommentEvent) {
|
||||
rejectCommentEvent.error({ message: error.message, code: error.code });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default RejectCommentMutation;
|
||||
|
||||
+52
-32
@@ -1,12 +1,14 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment, RecordSourceSelectorProxy } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
import { UnfeatureCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { UnfeatureCommentMutation as MutationTypes } from "coral-stream/__generated__/UnfeatureCommentMutation.graphql";
|
||||
|
||||
@@ -29,42 +31,60 @@ function decrementCount(store: RecordSourceSelectorProxy, storyID: string) {
|
||||
|
||||
const UnfeatureCommentMutation = createMutation(
|
||||
"unfeatureComment",
|
||||
(
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes> & { storyID: string }
|
||||
) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UnfeatureCommentMutation($input: UnfeatureCommentInput!) {
|
||||
unfeatureComment(input: $input) {
|
||||
comment {
|
||||
tags {
|
||||
code
|
||||
input: MutationInput<MutationTypes> & { storyID: string },
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const unfeaturedCommentEvent = UnfeatureCommentEvent.begin(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation UnfeatureCommentMutation($input: UnfeatureCommentInput!) {
|
||||
unfeatureComment(input: $input) {
|
||||
comment {
|
||||
tags {
|
||||
code
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
`,
|
||||
optimisticUpdater: store => {
|
||||
const comment = store.get(input.commentID)!;
|
||||
const tags = comment.getLinkedRecords("tags")!;
|
||||
comment.setLinkedRecords(
|
||||
tags.filter(t => t!.getValue("code") === GQLTAG.FEATURED),
|
||||
"tags"
|
||||
);
|
||||
decrementCount(store, input.storyID);
|
||||
},
|
||||
updater: store => {
|
||||
decrementCount(store, input.storyID);
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
optimisticUpdater: store => {
|
||||
const comment = store.get(input.commentID)!;
|
||||
const tags = comment.getLinkedRecords("tags")!;
|
||||
comment.setLinkedRecords(
|
||||
tags.filter(t => t!.getValue("code") === GQLTAG.FEATURED),
|
||||
"tags"
|
||||
);
|
||||
decrementCount(store, input.storyID);
|
||||
},
|
||||
updater: store => {
|
||||
decrementCount(store, input.storyID);
|
||||
},
|
||||
variables: {
|
||||
input: {
|
||||
commentID: input.commentID,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
unfeaturedCommentEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
unfeaturedCommentEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default UnfeatureCommentMutation;
|
||||
|
||||
@@ -34,7 +34,7 @@ const Permalink: FunctionComponent<PermalinkProps> = ({
|
||||
classes={{ popover: styles.popover }}
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside onClickOutside={toggleVisibility}>
|
||||
<PermalinkPopover permalinkURL={url} />
|
||||
<PermalinkPopover permalinkURL={url} commentID={commentID} />
|
||||
</ClickOutside>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
|
||||
import { CopyButton } from "coral-framework/components";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { CopyPermalinkEvent, ShowSharePopoverEvent } from "coral-stream/events";
|
||||
import { Flex, TextField } from "coral-ui/components";
|
||||
|
||||
import styles from "./PermalinkPopover.css";
|
||||
|
||||
interface Props {
|
||||
permalinkURL: string;
|
||||
commentID: string;
|
||||
}
|
||||
|
||||
class PermalinkPopover extends React.Component<Props> {
|
||||
public render() {
|
||||
const { permalinkURL } = this.props;
|
||||
return (
|
||||
<Flex
|
||||
itemGutter="half"
|
||||
className={cn(styles.root, CLASSES.sharePopover.$root)}
|
||||
>
|
||||
<TextField
|
||||
defaultValue={permalinkURL}
|
||||
className={styles.textField}
|
||||
readOnly
|
||||
/>
|
||||
<CopyButton
|
||||
text={permalinkURL}
|
||||
className={CLASSES.sharePopover.copyButton}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
const PermalinkPopover: FunctionComponent<Props> = ({
|
||||
permalinkURL,
|
||||
commentID,
|
||||
}) => {
|
||||
const emitShowEvent = useViewerEvent(ShowSharePopoverEvent);
|
||||
const emitCopyEvent = useViewerEvent(CopyPermalinkEvent);
|
||||
const onButtonClick = useCallback(() => emitCopyEvent({ commentID }), [
|
||||
emitCopyEvent,
|
||||
commentID,
|
||||
]);
|
||||
// Run once.
|
||||
useEffect(() => {
|
||||
emitShowEvent({ commentID });
|
||||
}, []);
|
||||
return (
|
||||
<Flex
|
||||
itemGutter="half"
|
||||
className={cn(styles.root, CLASSES.sharePopover.$root)}
|
||||
>
|
||||
<TextField
|
||||
defaultValue={permalinkURL}
|
||||
className={styles.textField}
|
||||
readOnly
|
||||
/>
|
||||
<CopyButton
|
||||
onClick={onButtonClick}
|
||||
text={permalinkURL}
|
||||
className={CLASSES.sharePopover.copyButton}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermalinkPopover;
|
||||
|
||||
+56
-30
@@ -2,6 +2,7 @@ import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
import { GQLComment } from "coral-framework/schema";
|
||||
|
||||
import { CreateCommentReactionMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentReactionMutation.graphql";
|
||||
import { CreateCommentReactionEvent } from "coral-stream/events";
|
||||
|
||||
export type CreateCommentReactionInput = MutationInput<MutationTypes>;
|
||||
|
||||
@@ -28,43 +30,67 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentReactionInput) {
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentReactionInput,
|
||||
{ eventEmitter }: Pick<CoralContext, "eventEmitter">
|
||||
) {
|
||||
const source = environment.getStore().getSource();
|
||||
const currentCount = source.get(
|
||||
source.get(source.get(input.commentID)!.actionCounts.__ref)!.reaction.__ref
|
||||
)!.total;
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "commentRevisionID"]),
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
createCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
viewerActionPresence: {
|
||||
reaction: true,
|
||||
const createCommentReactionEvent = CreateCommentReactionEvent.begin(
|
||||
eventEmitter,
|
||||
{
|
||||
commentID: input.commentID,
|
||||
}
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "commentRevisionID"]),
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
revision: {
|
||||
// comment revision should not be null since we just
|
||||
// reacted to it, revision can only be null when user
|
||||
// deletes their account and thus all their comments
|
||||
id: lookup<GQLComment>(environment, input.commentID)!.revision!.id,
|
||||
},
|
||||
optimisticResponse: {
|
||||
createCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
viewerActionPresence: {
|
||||
reaction: true,
|
||||
},
|
||||
revision: {
|
||||
// comment revision should not be null since we just
|
||||
// reacted to it, revision can only be null when user
|
||||
// deletes their account and thus all their comments
|
||||
id: lookup<GQLComment>(environment, input.commentID)!.revision!
|
||||
.id,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount + 1,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount + 1,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
createCommentReactionEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
createCommentReactionEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCreateCommentReactionMutation = createMutationContainer(
|
||||
|
||||
+56
-29
@@ -2,6 +2,7 @@ import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLComment } from "coral-framework/schema";
|
||||
import { RemoveCommentReactionEvent } from "coral-stream/events";
|
||||
|
||||
import { RemoveCommentReactionMutation as MutationTypes } from "coral-stream/__generated__/RemoveCommentReactionMutation.graphql";
|
||||
|
||||
@@ -28,41 +30,66 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: RemoveCommentReactionInput) {
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: RemoveCommentReactionInput,
|
||||
{ eventEmitter }: Pick<CoralContext, "eventEmitter">
|
||||
) {
|
||||
const source = environment.getStore().getSource();
|
||||
const currentCount = source.get(
|
||||
source.get(source.get(input.commentID)!.actionCounts.__ref)!.reaction.__ref
|
||||
)!.total;
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID"]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
|
||||
const removeCommentReactionEvent = RemoveCommentReactionEvent.begin(
|
||||
eventEmitter,
|
||||
{
|
||||
commentID: input.commentID,
|
||||
}
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID"]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
revision: {
|
||||
// Can assume revision exists since we just selected
|
||||
// to remove the reaction to it.
|
||||
id: lookup<GQLComment>(environment, input.commentID)!.revision!.id,
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeCommentReaction: {
|
||||
comment: {
|
||||
id: input.commentID,
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
},
|
||||
revision: {
|
||||
// Can assume revision exists since we just selected
|
||||
// to remove the reaction to it.
|
||||
id: lookup<GQLComment>(environment, input.commentID)!.revision!
|
||||
.id,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount - 1,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: currentCount - 1,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
removeCommentReactionEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
removeCommentReactionEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withRemoveCommentReactionMutation = createMutationContainer(
|
||||
|
||||
+93
-74
@@ -17,6 +17,7 @@ import {
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLComment, GQLStory, GQLUSER_ROLE } from "coral-framework/schema";
|
||||
import { CreateCommentReplyEvent } from "coral-stream/events";
|
||||
|
||||
import { CreateCommentReplyMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentReplyMutation.graphql";
|
||||
|
||||
@@ -142,10 +143,10 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentReplyInput,
|
||||
{ uuidGenerator, relayEnvironment }: CoralContext
|
||||
{ uuidGenerator, relayEnvironment, eventEmitter }: CoralContext
|
||||
) {
|
||||
const parentComment = lookup<GQLComment>(environment, input.parentID)!;
|
||||
const viewer = getViewer(environment)!;
|
||||
@@ -162,82 +163,100 @@ function commit(
|
||||
!roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF) &&
|
||||
storySettings.moderation === "PRE";
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
storyID: input.storyID,
|
||||
parentID: input.parentID,
|
||||
parentRevisionID: input.parentRevisionID,
|
||||
body: input.body,
|
||||
nudge: input.nudge,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
createCommentReply: {
|
||||
edge: {
|
||||
cursor: currentDate,
|
||||
node: {
|
||||
id,
|
||||
createdAt: currentDate,
|
||||
status: "NONE",
|
||||
author: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
const createCommentReplyEvent = CreateCommentReplyEvent.begin(eventEmitter, {
|
||||
body: input.body,
|
||||
parentID: input.parentID,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
storyID: input.storyID,
|
||||
parentID: input.parentID,
|
||||
parentRevisionID: input.parentRevisionID,
|
||||
body: input.body,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
parent: {
|
||||
id: parentComment.id,
|
||||
author: parentComment.author
|
||||
? pick(parentComment.author, "username", "id")
|
||||
: null,
|
||||
},
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000).toISOString(),
|
||||
edited: false,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
|
||||
? [{ code: "STAFF" }]
|
||||
: [],
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
dontAgree: false,
|
||||
flag: false,
|
||||
},
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
deleted: false,
|
||||
nudge: input.nudge,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
// Skip optimistic update if comment is probably premoderated.
|
||||
if (expectPremoderation) {
|
||||
return;
|
||||
optimisticResponse: {
|
||||
createCommentReply: {
|
||||
edge: {
|
||||
cursor: currentDate,
|
||||
node: {
|
||||
id,
|
||||
createdAt: currentDate,
|
||||
status: "NONE",
|
||||
author: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
body: input.body,
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
parent: {
|
||||
id: parentComment.id,
|
||||
author: parentComment.author
|
||||
? pick(parentComment.author, "username", "id")
|
||||
: null,
|
||||
},
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000).toISOString(),
|
||||
edited: false,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
|
||||
? [{ code: "STAFF" }]
|
||||
: [],
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
dontAgree: false,
|
||||
flag: false,
|
||||
},
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
deleted: false,
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
// Skip optimistic update if comment is probably premoderated.
|
||||
if (expectPremoderation) {
|
||||
return;
|
||||
}
|
||||
sharedUpdater(environment, store, input);
|
||||
store.get(id)!.setValue(true, "pending");
|
||||
},
|
||||
updater: store => {
|
||||
sharedUpdater(environment, store, input);
|
||||
},
|
||||
}
|
||||
sharedUpdater(environment, store, input);
|
||||
store.get(id)!.setValue(true, "pending");
|
||||
},
|
||||
updater: store => {
|
||||
sharedUpdater(environment, store, input);
|
||||
},
|
||||
});
|
||||
);
|
||||
createCommentReplyEvent.success({
|
||||
id: result.edge.node.id,
|
||||
status: result.edge.node.status,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
createCommentReplyEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCreateCommentReplyMutation = createMutationContainer(
|
||||
|
||||
@@ -2,12 +2,20 @@ import { CoralRTE } from "@coralproject/rte";
|
||||
import cn from "classnames";
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { EventHandler, FunctionComponent, MouseEvent, Ref } from "react";
|
||||
import React, {
|
||||
EventHandler,
|
||||
FunctionComponent,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { OnSubmit } from "coral-framework/lib/form";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import { ReplyCommentFocusEvent } from "coral-stream/events";
|
||||
import {
|
||||
AriaInfo,
|
||||
Button,
|
||||
@@ -42,6 +50,10 @@ export interface ReplyCommentFormProps {
|
||||
|
||||
const ReplyCommentForm: FunctionComponent<ReplyCommentFormProps> = props => {
|
||||
const inputID = `comments-replyCommentForm-rte-${props.id}`;
|
||||
const emitFocusEvent = useViewerEvent(ReplyCommentFocusEvent);
|
||||
const onFocus = useCallback(() => {
|
||||
emitFocusEvent();
|
||||
}, [emitFocusEvent]);
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, form, submitError }) => (
|
||||
@@ -78,6 +90,7 @@ const ReplyCommentForm: FunctionComponent<ReplyCommentFormProps> = props => {
|
||||
>
|
||||
<RTE
|
||||
inputId={inputID}
|
||||
onFocus={onFocus}
|
||||
onChange={({ html }) =>
|
||||
input.onChange(cleanupRTEEmptyHTML(html))
|
||||
}
|
||||
|
||||
+34
-9
@@ -2,12 +2,14 @@ import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ReportCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { CreateCommentDontAgreeMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentDontAgreeMutation.graphql";
|
||||
|
||||
@@ -31,16 +33,39 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentDontAgreeInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, ["commentID", "commentRevisionID", "additionalDetails"]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentDontAgreeInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) {
|
||||
const reportCommentEvent = ReportCommentEvent.begin(eventEmitter, {
|
||||
reason: "DONT_AGREE",
|
||||
additionalDetails: input.additionalDetails || undefined,
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, [
|
||||
"commentID",
|
||||
"commentRevisionID",
|
||||
"additionalDetails",
|
||||
]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
reportCommentEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
reportCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCreateCommentDontAgreeMutation = createMutationContainer(
|
||||
|
||||
+34
-14
@@ -2,12 +2,14 @@ import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ReportCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { CreateCommentFlagMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentFlagMutation.graphql";
|
||||
|
||||
@@ -29,21 +31,39 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentFlagInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, [
|
||||
"commentID",
|
||||
"commentRevisionID",
|
||||
"reason",
|
||||
"additionalDetails",
|
||||
]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentFlagInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) {
|
||||
const reportCommentEvent = ReportCommentEvent.begin(eventEmitter, {
|
||||
reason: input.reason,
|
||||
commentID: input.commentID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...pick(input, [
|
||||
"commentID",
|
||||
"commentRevisionID",
|
||||
"reason",
|
||||
"additionalDetails",
|
||||
]),
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
reportCommentEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
reportCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCreateCommentFlagMutation = createMutationContainer(
|
||||
|
||||
+24
-10
@@ -1,9 +1,12 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import React, { Component } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withContext } from "coral-framework/lib/bootstrap";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { ShowReportPopoverEvent } from "coral-stream/events";
|
||||
|
||||
import { ReportCommentFormContainer_comment as CommentData } from "coral-stream/__generated__/ReportCommentFormContainer_comment.graphql";
|
||||
|
||||
@@ -19,6 +22,7 @@ import ReportCommentForm from "./ReportCommentForm";
|
||||
import ThankYou from "./ThankYou";
|
||||
|
||||
interface Props {
|
||||
eventEmitter: EventEmitter2;
|
||||
comment: CommentData;
|
||||
createCommentFlag: CreateCommentFlagMutation;
|
||||
createCommentDontAgree: CreateCommentDontAgreeMutation;
|
||||
@@ -66,6 +70,12 @@ export class ReportCommentFormContainer extends Component<Props, State> {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
ShowReportPopoverEvent.emit(this.props.eventEmitter, {
|
||||
commentID: this.props.comment.id,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
// Reposition popper after switching view.
|
||||
if (this.state.done && !prevState.done) {
|
||||
@@ -88,18 +98,22 @@ export class ReportCommentFormContainer extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withCreateCommentDontAgreeMutation(
|
||||
withCreateCommentFlagMutation(
|
||||
withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment ReportCommentFormContainer_comment on Comment {
|
||||
id
|
||||
revision {
|
||||
const enhanced = withContext(({ eventEmitter }) => ({
|
||||
eventEmitter,
|
||||
}))(
|
||||
withCreateCommentDontAgreeMutation(
|
||||
withCreateCommentFlagMutation(
|
||||
withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment ReportCommentFormContainer_comment on Comment {
|
||||
id
|
||||
revision {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ReportCommentFormContainer)
|
||||
`,
|
||||
})(ReportCommentFormContainer)
|
||||
)
|
||||
)
|
||||
);
|
||||
export type ReportCommentFormContainerProps = PropTypesOf<typeof enhanced>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { BaseButton, Icon } from "coral-ui/components";
|
||||
@@ -15,26 +15,27 @@ interface Props {
|
||||
onResize: () => void;
|
||||
}
|
||||
|
||||
class ReportPopover extends React.Component<Props> {
|
||||
public render() {
|
||||
const { onClose, onResize, comment } = this.props;
|
||||
return (
|
||||
<div className={cn(styles.root, CLASSES.reportPopover.$root)}>
|
||||
<BaseButton
|
||||
onClick={onClose}
|
||||
className={cn(styles.close, CLASSES.reportPopover.closeButton)}
|
||||
aria-label="Close Popover"
|
||||
>
|
||||
<Icon>close</Icon>
|
||||
</BaseButton>
|
||||
<ReportCommentFormContainer
|
||||
comment={comment}
|
||||
onClose={onClose}
|
||||
onResize={onResize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
const ReportPopover: FunctionComponent<Props> = ({
|
||||
onClose,
|
||||
onResize,
|
||||
comment,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(styles.root, CLASSES.reportPopover.$root)}>
|
||||
<BaseButton
|
||||
onClick={onClose}
|
||||
className={cn(styles.close, CLASSES.reportPopover.closeButton)}
|
||||
aria-label="Close Popover"
|
||||
>
|
||||
<Icon>close</Icon>
|
||||
</BaseButton>
|
||||
<ReportCommentFormContainer
|
||||
comment={comment}
|
||||
onClose={onClose}
|
||||
onResize={onResize}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportPopover;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ exports[`renders correctly 1`] = `
|
||||
close
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(ReportCommentFormContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(ReportCommentFormContainer))))))
|
||||
comment={Object {}}
|
||||
onClose={[Function]}
|
||||
onResize={[Function]}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Flex, Timestamp } from "coral-ui/components";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import { Flex } from "coral-ui/components";
|
||||
|
||||
import TopBarLeft from "./TopBarLeft";
|
||||
import Username from "./Username";
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { BanUserEvent } from "coral-stream/events";
|
||||
|
||||
import { BanUserMutation } from "coral-stream/__generated__/BanUserMutation.graphql";
|
||||
|
||||
@@ -13,43 +15,62 @@ let clientMutationId = 0;
|
||||
|
||||
const BanUserMutation = createMutation(
|
||||
"banUser",
|
||||
(environment: Environment, input: MutationInput<BanUserMutation>) => {
|
||||
return commitMutationPromiseNormalized<BanUserMutation>(environment, {
|
||||
mutation: graphql`
|
||||
mutation BanUserMutation($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
ban {
|
||||
active
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<BanUserMutation> & { commentID: string },
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const banUserEvent = BanUserEvent.begin(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
userID: input.userID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<BanUserMutation>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation BanUserMutation($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
ban {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
banUser: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
ban: {
|
||||
active: true,
|
||||
},
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
message: input.message,
|
||||
userID: input.userID,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
optimisticResponse: {
|
||||
banUser: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
ban: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
banUserEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
banUserEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+2
-1
@@ -37,6 +37,7 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
|
||||
const onBan = useCallback(() => {
|
||||
banUser({
|
||||
userID: user.id,
|
||||
commentID: comment.id,
|
||||
message: getMessage(
|
||||
localeBundles,
|
||||
"common-banEmailTemplate",
|
||||
@@ -48,7 +49,7 @@ const UserBanPopoverContainer: FunctionComponent<Props> = ({
|
||||
reject({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: story.id,
|
||||
noEmit: true,
|
||||
});
|
||||
}
|
||||
onDismiss();
|
||||
|
||||
+48
-26
@@ -2,11 +2,13 @@ import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { getViewer } from "coral-framework/helpers";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { IgnoreUserEvent } from "coral-stream/events";
|
||||
|
||||
import { IgnoreUserMutation as MutationTypes } from "coral-stream/__generated__/IgnoreUserMutation.graphql";
|
||||
|
||||
@@ -14,33 +16,53 @@ let clientMutationId = 0;
|
||||
|
||||
const IgnoreUserMutation = createMutation(
|
||||
"ignoreUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation IgnoreUserMutation($input: IgnoreUserInput!) {
|
||||
ignoreUser(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const ignoreUserEvent = IgnoreUserEvent.begin(eventEmitter, {
|
||||
userID: input.userID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation IgnoreUserMutation($input: IgnoreUserInput!) {
|
||||
ignoreUser(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const viewerProxy = store.get(viewer.id)!;
|
||||
const ignoredUserRecords = viewerProxy.getLinkedRecords(
|
||||
"ignoredUsers"
|
||||
);
|
||||
if (ignoredUserRecords) {
|
||||
viewerProxy.setLinkedRecords(
|
||||
ignoredUserRecords.concat(store.get(input.userID)),
|
||||
"ignoredUsers"
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const viewerProxy = store.get(viewer.id)!;
|
||||
const ignoredUserRecords = viewerProxy.getLinkedRecords("ignoredUsers");
|
||||
if (ignoredUserRecords) {
|
||||
viewerProxy.setLinkedRecords(
|
||||
ignoredUserRecords.concat(store.get(input.userID)),
|
||||
"ignoredUsers"
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
ignoreUserEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
ignoreUserEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default IgnoreUserMutation;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { ShowUserPopoverEvent } from "coral-stream/events";
|
||||
|
||||
import { UserPopoverContainer_user as UserData } from "coral-stream/__generated__/UserPopoverContainer_user.graphql";
|
||||
import { UserPopoverContainer_viewer as ViewerData } from "coral-stream/__generated__/UserPopoverContainer_viewer.graphql";
|
||||
@@ -22,6 +29,10 @@ export const UserPopoverContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const emitShowUserPopover = useViewerEvent(ShowUserPopoverEvent);
|
||||
useEffect(() => {
|
||||
emitShowUserPopover({ userID: user.id });
|
||||
}, []);
|
||||
const [view, setView] = useState<View>("OVERVIEW");
|
||||
const onIgnore = useCallback(() => setView("IGNORE"), [setView]);
|
||||
return (
|
||||
@@ -47,6 +58,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
`,
|
||||
user: graphql`
|
||||
fragment UserPopoverContainer_user on User {
|
||||
id
|
||||
...UserPopoverOverviewContainer_user
|
||||
...UserIgnorePopoverContainer_user
|
||||
}
|
||||
|
||||
@@ -24,12 +24,11 @@ exports[`renders username and body 1`] = `
|
||||
direction="row"
|
||||
itemGutter={true}
|
||||
>
|
||||
<Timestamp
|
||||
<TimeStamp
|
||||
className="coral coral-timestamp coral-comment-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
1995-12-17T03:24:00.000Z
|
||||
</Timestamp>
|
||||
</TimeStamp>
|
||||
<EditedMarker
|
||||
className="coral coral-comment-edited"
|
||||
/>
|
||||
|
||||
+120
-122
@@ -1,13 +1,18 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
import React from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withContext } from "coral-framework/lib/bootstrap";
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import Counter from "coral-stream/common/Counter";
|
||||
import { ShowMoreOfConversationEvent } from "coral-stream/events";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
@@ -30,7 +35,7 @@ import { Circle, Line } from "./Timeline";
|
||||
|
||||
import styles from "./ConversationThreadContainer.css";
|
||||
|
||||
interface ConversationThreadContainerProps {
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
story: StoryData;
|
||||
settings: SettingsData;
|
||||
@@ -40,133 +45,126 @@ interface ConversationThreadContainerProps {
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
class ConversationThreadContainer extends React.Component<
|
||||
ConversationThreadContainerProps
|
||||
> {
|
||||
public state = {
|
||||
disableLoadMore: false,
|
||||
};
|
||||
|
||||
private loadMore = () => {
|
||||
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
|
||||
return;
|
||||
const ConversationThreadContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
story,
|
||||
viewer,
|
||||
settings,
|
||||
relay,
|
||||
}) => {
|
||||
const [loadMore, isLoadingMore] = useLoadMore(relay, 5);
|
||||
const beginLoadMoreEvent = useViewerNetworkEvent(ShowMoreOfConversationEvent);
|
||||
const loadMoreAndEmit = useCallback(async () => {
|
||||
const loadMoreEvent = beginLoadMoreEvent({ commentID: comment.id });
|
||||
try {
|
||||
await loadMore();
|
||||
loadMoreEvent.success();
|
||||
} catch (error) {
|
||||
loadMoreEvent.error({ message: error.message, code: error.code });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
this.setState({ disableLoadMore: true });
|
||||
this.props.relay.loadMore(
|
||||
5, // Fetch the next 5 feed items
|
||||
error => {
|
||||
this.setState({ disableLoadMore: false });
|
||||
if (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}, [loadMore, beginLoadMoreEvent]);
|
||||
const parents = comment.parents.edges.map(edge => edge.node);
|
||||
const remaining = comment.parentCount - comment.parents.edges.length;
|
||||
const hasMore = relay.hasMore();
|
||||
const rootParent = hasMore && comment && comment.rootParent;
|
||||
|
||||
const dataTestID = "comments-permalinkView-conversationThread";
|
||||
if (remaining === 0 && parents.length === 0) {
|
||||
return (
|
||||
<div className={styles.root} data-testid={dataTestID}>
|
||||
<CommentContainer
|
||||
comment={comment}
|
||||
story={story}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
highlight
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { comment, story, viewer, settings } = this.props;
|
||||
const parents = comment.parents.edges.map(edge => edge.node);
|
||||
const remaining = comment.parentCount - comment.parents.edges.length;
|
||||
const hasMore = this.props.relay.hasMore();
|
||||
const rootParent = hasMore && comment && comment.rootParent;
|
||||
|
||||
const dataTestID = "comments-permalinkView-conversationThread";
|
||||
if (remaining === 0 && parents.length === 0) {
|
||||
return (
|
||||
<div className={styles.root} data-testid={dataTestID}>
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(CLASSES.conversationThread.$root, styles.root)}
|
||||
data-testid={dataTestID}
|
||||
>
|
||||
<HorizontalGutter container={<Line dotted />}>
|
||||
{rootParent && (
|
||||
<Circle>
|
||||
<RootParent
|
||||
id={rootParent.id}
|
||||
username={rootParent.author && rootParent.author.username}
|
||||
createdAt={rootParent.createdAt}
|
||||
tags={
|
||||
<UserTagsContainer
|
||||
className={CLASSES.conversationThread.rootParent.userTag}
|
||||
comment={rootParent}
|
||||
settings={settings}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Circle>
|
||||
)}
|
||||
{remaining > 0 && (
|
||||
<Circle hollow className={styles.loadMore}>
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
<Localized
|
||||
id="comments-conversationThread-showMoreOfThisConversation"
|
||||
$count={remaining}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
CLASSES.conversationThread.showMore,
|
||||
styles.showMoreButton
|
||||
)}
|
||||
onClick={loadMoreAndEmit}
|
||||
disabled={isLoadingMore}
|
||||
variant="underlined"
|
||||
>
|
||||
Show more of this conversation
|
||||
</Button>
|
||||
</Localized>
|
||||
{remaining > 1 && <Counter color="dark">{remaining}</Counter>}
|
||||
</Flex>
|
||||
</Circle>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter container={Line}>
|
||||
{parents.map((parent, i) => (
|
||||
<Circle key={parent.id} hollow={!!remaining || i > 0}>
|
||||
<CommentContainer
|
||||
comment={parent}
|
||||
story={story}
|
||||
viewer={viewer}
|
||||
settings={settings}
|
||||
localReply
|
||||
/>
|
||||
{viewer && (
|
||||
<LocalReplyListContainer
|
||||
story={story}
|
||||
viewer={viewer}
|
||||
settings={settings}
|
||||
comment={parent}
|
||||
indentLevel={1}
|
||||
/>
|
||||
)}
|
||||
</Circle>
|
||||
))}
|
||||
<Circle end>
|
||||
<CommentContainer
|
||||
className={CLASSES.conversationThread.hightlighted}
|
||||
comment={comment}
|
||||
story={story}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
highlight
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn(CLASSES.conversationThread.$root, styles.root)}
|
||||
data-testid={dataTestID}
|
||||
>
|
||||
<HorizontalGutter container={<Line dotted />}>
|
||||
{rootParent && (
|
||||
<Circle>
|
||||
<RootParent
|
||||
id={rootParent.id}
|
||||
username={rootParent.author && rootParent.author.username}
|
||||
createdAt={rootParent.createdAt}
|
||||
tags={
|
||||
<UserTagsContainer
|
||||
className={CLASSES.conversationThread.rootParent.userTag}
|
||||
comment={rootParent}
|
||||
settings={settings}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Circle>
|
||||
)}
|
||||
{remaining > 0 && (
|
||||
<Circle hollow className={styles.loadMore}>
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
<Localized
|
||||
id="comments-conversationThread-showMoreOfThisConversation"
|
||||
$count={remaining}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
CLASSES.conversationThread.showMore,
|
||||
styles.showMoreButton
|
||||
)}
|
||||
onClick={this.loadMore}
|
||||
disabled={this.state.disableLoadMore}
|
||||
variant="underlined"
|
||||
>
|
||||
Show more of this conversation
|
||||
</Button>
|
||||
</Localized>
|
||||
{remaining > 1 && <Counter color="dark">{remaining}</Counter>}
|
||||
</Flex>
|
||||
</Circle>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter container={Line}>
|
||||
{parents.map((parent, i) => (
|
||||
<Circle key={parent.id} hollow={!!remaining || i > 0}>
|
||||
<CommentContainer
|
||||
comment={parent}
|
||||
story={story}
|
||||
viewer={viewer}
|
||||
settings={settings}
|
||||
localReply
|
||||
/>
|
||||
{viewer && (
|
||||
<LocalReplyListContainer
|
||||
story={story}
|
||||
viewer={viewer}
|
||||
settings={settings}
|
||||
comment={parent}
|
||||
indentLevel={1}
|
||||
/>
|
||||
)}
|
||||
</Circle>
|
||||
))}
|
||||
<Circle end>
|
||||
<CommentContainer
|
||||
className={CLASSES.conversationThread.hightlighted}
|
||||
comment={comment}
|
||||
story={story}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
highlight
|
||||
/>
|
||||
</Circle>
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</Circle>
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: (cvle) This should be autogenerated.
|
||||
interface FragmentVariables {
|
||||
@@ -179,7 +177,7 @@ const enhanced = withContext(ctx => ({
|
||||
}))(
|
||||
withSetCommentIDMutation(
|
||||
withPaginationContainer<
|
||||
ConversationThreadContainerProps,
|
||||
Props,
|
||||
ConversationThreadContainerPaginationQueryVariables,
|
||||
FragmentVariables
|
||||
>(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
import React, { MouseEvent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
@@ -5,6 +6,7 @@ import { graphql } from "react-relay";
|
||||
import { getURLWithCommentID } from "coral-framework/helpers";
|
||||
import { withContext } from "coral-framework/lib/bootstrap";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { ViewFullDiscussionEvent } from "coral-stream/events";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
@@ -24,12 +26,16 @@ interface PermalinkViewContainerProps {
|
||||
viewer: ViewerData | null;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
pym: PymChild | undefined;
|
||||
eventEmitter: EventEmitter2;
|
||||
}
|
||||
|
||||
class PermalinkViewContainer extends React.Component<
|
||||
PermalinkViewContainerProps
|
||||
> {
|
||||
private showAllComments = (e: MouseEvent<any>) => {
|
||||
ViewFullDiscussionEvent.emit(this.props.eventEmitter, {
|
||||
commentID: this.props.comment && this.props.comment.id,
|
||||
});
|
||||
this.props.setCommentID({ id: null });
|
||||
e.preventDefault();
|
||||
};
|
||||
@@ -62,6 +68,7 @@ class PermalinkViewContainer extends React.Component<
|
||||
|
||||
const enhanced = withContext(ctx => ({
|
||||
pym: ctx.pym,
|
||||
eventEmitter: ctx.eventEmitter,
|
||||
}))(
|
||||
withSetCommentIDMutation(
|
||||
withFragmentContainer<PermalinkViewContainerProps>({
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ exports[`renders comment not found 1`] = `
|
||||
className="PermalinkView-root coral coral-permalink coral-authenticated"
|
||||
size="double"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
<withContext(withMutation(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
settings={Object {}}
|
||||
viewer={Object {}}
|
||||
/>
|
||||
@@ -67,7 +67,7 @@ exports[`renders correctly 1`] = `
|
||||
className="PermalinkView-root coral coral-permalink coral-authenticated"
|
||||
size="double"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
<withContext(withMutation(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
settings={Object {}}
|
||||
viewer={Object {}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Blockquote, Bold, CoralRTE, Italic } from "@coralproject/rte";
|
||||
import cn from "classnames";
|
||||
import { Localized as LocalizedOriginal } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, Ref } from "react";
|
||||
import React, { EventHandler, FocusEvent, FunctionComponent, Ref } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Icon } from "coral-ui/components";
|
||||
@@ -67,6 +67,8 @@ export interface RTEProps {
|
||||
* onChange
|
||||
*/
|
||||
onChange?: (data: { html: string; text: string }) => void;
|
||||
onFocus?: EventHandler<FocusEvent>;
|
||||
onBlur?: EventHandler<FocusEvent>;
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -109,6 +111,8 @@ const RTE: FunctionComponent<RTEProps> = props => {
|
||||
contentClassName,
|
||||
placeholderClassName,
|
||||
toolbarClassName,
|
||||
onFocus,
|
||||
onBlur,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
@@ -138,6 +142,8 @@ const RTE: FunctionComponent<RTEProps> = props => {
|
||||
features={features}
|
||||
ref={forwardRef}
|
||||
toolbarPosition="bottom"
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
@@ -11,6 +12,10 @@ import { ReplyListContainer } from "./ReplyListContainer";
|
||||
// Remove relay refs so we can stub the props.
|
||||
const ReplyListContainerN = removeFragmentRefs(ReplyListContainer);
|
||||
|
||||
/* Mock useContext */
|
||||
const context = { eventEmitter: new EventEmitter2() };
|
||||
jest.spyOn(React, "useContext").mockImplementation(() => context);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
story: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
import { graphql, GraphQLTaggedNode, RelayPaginationProp } from "react-relay";
|
||||
import { withProps } from "recompose";
|
||||
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
useLoadMore,
|
||||
useMutation,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
import { FragmentKeys } from "coral-framework/lib/relay/types";
|
||||
import { Omit, PropTypesOf } from "coral-framework/types";
|
||||
import { ShowAllRepliesEvent } from "coral-stream/events";
|
||||
|
||||
import { ReplyListContainer1_comment as CommentData } from "coral-stream/__generated__/ReplyListContainer1_comment.graphql";
|
||||
import { ReplyListContainer1_settings as SettingsData } from "coral-stream/__generated__/ReplyListContainer1_settings.graphql";
|
||||
@@ -58,6 +60,19 @@ type FragmentVariables = Omit<
|
||||
|
||||
export const ReplyListContainer: React.FunctionComponent<Props> = props => {
|
||||
const [showAll, isLoadingShowAll] = useLoadMore(props.relay, 999999999);
|
||||
const beginShowAllEvent = useViewerNetworkEvent(ShowAllRepliesEvent);
|
||||
const showAllAndEmit = useCallback(async () => {
|
||||
const showAllEvent = beginShowAllEvent({ commentID: props.comment.id });
|
||||
try {
|
||||
await showAll();
|
||||
showAllEvent.success();
|
||||
} catch (error) {
|
||||
showAllEvent.error({ message: error.message, code: error.code });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}, [showAll, beginShowAllEvent, props.comment.id]);
|
||||
|
||||
const subcribeToCommentReplyCreated = useSubscription(
|
||||
CommentReplyCreatedSubscription
|
||||
);
|
||||
@@ -129,7 +144,7 @@ export const ReplyListContainer: React.FunctionComponent<Props> = props => {
|
||||
comments={comments}
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
onShowAll={showAll}
|
||||
onShowAll={showAllAndEmit}
|
||||
hasMore={props.relay.hasMore()}
|
||||
disableShowAll={isLoadingShowAll}
|
||||
indentLevel={props.indentLevel}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ConnectionHandler, Environment, RecordProxy } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ShowMoreRepliesEvent } from "coral-stream/events";
|
||||
|
||||
interface ReplyListViewNewInput {
|
||||
commentID: string;
|
||||
@@ -11,7 +13,11 @@ interface ReplyListViewNewInput {
|
||||
|
||||
const QueueViewNewMutation = createMutation(
|
||||
"viewNew",
|
||||
async (environment: Environment, input: ReplyListViewNewInput) => {
|
||||
async (
|
||||
environment: Environment,
|
||||
input: ReplyListViewNewInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
await commitLocalUpdatePromisified(environment, async store => {
|
||||
const parentProxy = store.get(input.commentID);
|
||||
if (!parentProxy) {
|
||||
@@ -37,6 +43,10 @@ const QueueViewNewMutation = createMutation(
|
||||
ConnectionHandler.insertEdgeAfter(connection, edge);
|
||||
});
|
||||
connection.setLinkedRecords([], "viewNewEdges");
|
||||
ShowMoreRepliesEvent.emit(eventEmitter, {
|
||||
commentID: input.commentID,
|
||||
count: viewNewEdges.length,
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
+6
-6
@@ -21,7 +21,7 @@ exports[`renders correctly 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
@@ -66,7 +66,7 @@ exports[`renders correctly 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
@@ -120,7 +120,7 @@ exports[`when there is more disables load more button 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
@@ -162,7 +162,7 @@ exports[`when there is more disables load more button 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
@@ -233,7 +233,7 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-1"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
@@ -275,7 +275,7 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
<ForwardRef(forwardRef)
|
||||
key="comment-2"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer)))))
|
||||
<withContext(withContext(createMutationContainer(withContext(createMutationContainer(Relay(CommentContainer))))))
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
|
||||
+15
-1
@@ -3,6 +3,7 @@ import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
useLoadMore,
|
||||
useLocal,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { GQLCOMMENT_SORT } from "coral-framework/schema";
|
||||
import { Omit, PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { LoadMoreAllCommentsEvent } from "coral-stream/events";
|
||||
import { Box, Button, CallOut, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import { AllCommentsTabContainer_settings } from "coral-stream/__generated__/AllCommentsTabContainer_settings.graphql";
|
||||
@@ -104,6 +106,18 @@ export const AllCommentsTabContainer: FunctionComponent<Props> = props => {
|
||||
props.story.settings.live.enabled,
|
||||
]);
|
||||
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||
const beginLoadMoreEvent = useViewerNetworkEvent(LoadMoreAllCommentsEvent);
|
||||
const loadMoreAndEmit = useCallback(async () => {
|
||||
const loadMoreEvent = beginLoadMoreEvent({ storyID: props.story.id });
|
||||
try {
|
||||
await loadMore();
|
||||
loadMoreEvent.success();
|
||||
} catch (error) {
|
||||
loadMoreEvent.error({ message: error.message, code: error.code });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}, [loadMore, beginLoadMoreEvent, props.story.id]);
|
||||
const viewMore = useMutation(AllCommentsTabViewNewMutation);
|
||||
const onViewMore = useCallback(() => viewMore({ storyID: props.story.id }), [
|
||||
props.story.id,
|
||||
@@ -174,7 +188,7 @@ export const AllCommentsTabContainer: FunctionComponent<Props> = props => {
|
||||
{props.relay.hasMore() && (
|
||||
<Localized id="comments-loadMore">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
onClick={loadMoreAndEmit}
|
||||
variant="outlineFilled"
|
||||
fullWidth
|
||||
disabled={isLoadingMore}
|
||||
|
||||
+11
-1
@@ -1,10 +1,12 @@
|
||||
import { ConnectionHandler, Environment, RecordProxy } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
createMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_SORT } from "coral-framework/schema";
|
||||
import { ViewNewCommentsEvent } from "coral-stream/events";
|
||||
|
||||
interface Input {
|
||||
storyID: string;
|
||||
@@ -12,7 +14,11 @@ interface Input {
|
||||
|
||||
const AllCommentsTabViewNewMutation = createMutation(
|
||||
"viewNew",
|
||||
async (environment: Environment, input: Input) => {
|
||||
async (
|
||||
environment: Environment,
|
||||
input: Input,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
await commitLocalUpdatePromisified(environment, async store => {
|
||||
const story = store.get(input.storyID)!;
|
||||
const connection = ConnectionHandler.getConnection(
|
||||
@@ -29,6 +35,10 @@ const AllCommentsTabViewNewMutation = createMutation(
|
||||
viewNewEdges.forEach(edge => {
|
||||
ConnectionHandler.insertEdgeBefore(connection, edge);
|
||||
});
|
||||
ViewNewCommentsEvent.emit(eventEmitter, {
|
||||
storyID: input.storyID,
|
||||
count: viewNewEdges.length,
|
||||
});
|
||||
connection.setLinkedRecords([], "viewNewEdges");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useEffect } from "react";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { ShowFeaturedCommentTooltipEvent } from "coral-stream/events";
|
||||
import { Tooltip, TooltipButton } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
@@ -8,6 +10,18 @@ interface Props {
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const FeaturedCommentTooltipContent: FunctionComponent = props => {
|
||||
const emitShowTooltipEvent = useViewerEvent(ShowFeaturedCommentTooltipEvent);
|
||||
useEffect(() => {
|
||||
emitShowTooltipEvent();
|
||||
}, []);
|
||||
return (
|
||||
<Localized id="comments-featuredCommentTooltip-handSelectedComments">
|
||||
<span>Comments are hand selected by our team as worth reading.</span>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeaturedCommentTooltip: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -18,12 +32,8 @@ export const FeaturedCommentTooltip: FunctionComponent<Props> = props => {
|
||||
<span>How is a comment featured?</span>
|
||||
</Localized>
|
||||
}
|
||||
body={
|
||||
<Localized id="comments-featuredCommentTooltip-handSelectedComments">
|
||||
<span>Comments are hand selected by our team as worth reading.</span>
|
||||
</Localized>
|
||||
}
|
||||
button={({ toggleVisibility, ref }) => (
|
||||
body={<FeaturedCommentTooltipContent />}
|
||||
button={({ toggleVisibility, ref, visible }) => (
|
||||
<Localized
|
||||
id="comments-featuredCommentTooltip-toggleButton"
|
||||
attrs={{ "aria-label": true }}
|
||||
|
||||
+9
-1
@@ -4,15 +4,18 @@ import React, { FunctionComponent, MouseEvent, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { getURLWithCommentID } from "coral-framework/helpers";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
|
||||
import { GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import HTMLContent from "coral-stream/common/HTMLContent";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import { ViewConversationEvent } from "coral-stream/events";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
} from "coral-stream/mutations";
|
||||
import { Box, Flex, Icon, TextLink, Timestamp } from "coral-ui/components";
|
||||
import { Box, Flex, Icon, TextLink } from "coral-ui/components";
|
||||
|
||||
import { FeaturedCommentContainer_comment as CommentData } from "coral-stream/__generated__/FeaturedCommentContainer_comment.graphql";
|
||||
import { FeaturedCommentContainer_settings as SettingsData } from "coral-stream/__generated__/FeaturedCommentContainer_settings.graphql";
|
||||
@@ -38,9 +41,14 @@ const FeaturedCommentContainer: FunctionComponent<Props> = props => {
|
||||
const banned = Boolean(
|
||||
viewer && viewer.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
);
|
||||
const emitViewConversationEvent = useViewerEvent(ViewConversationEvent);
|
||||
const onGotoConversation = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
emitViewConversationEvent({
|
||||
from: "FEATURED_COMMENTS",
|
||||
commentID: comment.id,
|
||||
});
|
||||
setCommentID({ id: comment.id });
|
||||
return false;
|
||||
},
|
||||
|
||||
+18
-2
@@ -1,13 +1,15 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { Omit, PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { LoadMoreFeaturedCommentsEvent } from "coral-stream/events";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import { FeaturedCommentsContainer_settings as SettingsData } from "coral-stream/__generated__/FeaturedCommentsContainer_settings.graphql";
|
||||
@@ -27,6 +29,20 @@ interface Props {
|
||||
|
||||
export const FeaturedCommentsContainer: FunctionComponent<Props> = props => {
|
||||
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||
const beginLoadMoreEvent = useViewerNetworkEvent(
|
||||
LoadMoreFeaturedCommentsEvent
|
||||
);
|
||||
const loadMoreAndEmit = useCallback(async () => {
|
||||
const loadMoreEvent = beginLoadMoreEvent({ storyID: props.story.id });
|
||||
try {
|
||||
await loadMore();
|
||||
loadMoreEvent.success();
|
||||
} catch (error) {
|
||||
loadMoreEvent.error({ message: error.message, code: error.code });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}, [loadMore, beginLoadMoreEvent, props.story.id]);
|
||||
const comments = props.story.featuredComments.edges.map(edge => edge.node);
|
||||
return (
|
||||
<>
|
||||
@@ -54,7 +70,7 @@ export const FeaturedCommentsContainer: FunctionComponent<Props> = props => {
|
||||
{props.relay.hasMore() && (
|
||||
<Localized id="comments-loadMore">
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
onClick={loadMoreAndEmit}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
disabled={isLoadingMore}
|
||||
|
||||
+86
-67
@@ -16,6 +16,7 @@ import {
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLStory, GQLUSER_ROLE } from "coral-framework/schema";
|
||||
import { CreateCommentEvent } from "coral-stream/events";
|
||||
|
||||
import { CreateCommentMutation as MutationTypes } from "coral-stream/__generated__/CreateCommentMutation.graphql";
|
||||
|
||||
@@ -114,10 +115,10 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentInput,
|
||||
{ uuidGenerator, relayEnvironment }: CoralContext
|
||||
{ uuidGenerator, relayEnvironment, eventEmitter }: CoralContext
|
||||
) {
|
||||
const viewer = getViewer(environment)!;
|
||||
const currentDate = new Date().toISOString();
|
||||
@@ -134,75 +135,93 @@ function commit(
|
||||
!roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF) &&
|
||||
storySettings.moderation === "PRE";
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
storyID: input.storyID,
|
||||
body: input.body,
|
||||
nudge: input.nudge,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
createComment: {
|
||||
edge: {
|
||||
cursor: currentDate,
|
||||
node: {
|
||||
id,
|
||||
createdAt: currentDate,
|
||||
status: "NONE",
|
||||
author: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
parent: null,
|
||||
const createCommentEvent = CreateCommentEvent.begin(eventEmitter, {
|
||||
body: input.body,
|
||||
storyID: input.storyID,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
storyID: input.storyID,
|
||||
body: input.body,
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000).toISOString(),
|
||||
edited: false,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
|
||||
? [{ code: "STAFF" }]
|
||||
: [],
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
dontAgree: false,
|
||||
flag: false,
|
||||
},
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
deleted: false,
|
||||
nudge: input.nudge,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
// Skip optimistic update if comment is probably premoderated.
|
||||
if (expectPremoderation) {
|
||||
return;
|
||||
optimisticResponse: {
|
||||
createComment: {
|
||||
edge: {
|
||||
cursor: currentDate,
|
||||
node: {
|
||||
id,
|
||||
createdAt: currentDate,
|
||||
status: "NONE",
|
||||
author: {
|
||||
id: viewer.id,
|
||||
username: viewer.username,
|
||||
createdAt: viewer.createdAt,
|
||||
badges: viewer.badges,
|
||||
ignoreable: false,
|
||||
},
|
||||
revision: {
|
||||
id: uuidGenerator(),
|
||||
},
|
||||
parent: null,
|
||||
body: input.body,
|
||||
editing: {
|
||||
editableUntil: new Date(Date.now() + 10000).toISOString(),
|
||||
edited: false,
|
||||
},
|
||||
actionCounts: {
|
||||
reaction: {
|
||||
total: 0,
|
||||
},
|
||||
},
|
||||
tags: roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF)
|
||||
? [{ code: "STAFF" }]
|
||||
: [],
|
||||
viewerActionPresence: {
|
||||
reaction: false,
|
||||
dontAgree: false,
|
||||
flag: false,
|
||||
},
|
||||
replies: {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
},
|
||||
deleted: false,
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
// Skip optimistic update if comment is probably premoderated.
|
||||
if (expectPremoderation) {
|
||||
return;
|
||||
}
|
||||
sharedUpdater(environment, store, input);
|
||||
store.get(id)!.setValue(true, "pending");
|
||||
},
|
||||
updater: store => {
|
||||
sharedUpdater(environment, store, input);
|
||||
},
|
||||
}
|
||||
sharedUpdater(environment, store, input);
|
||||
store.get(id)!.setValue(true, "pending");
|
||||
},
|
||||
updater: store => {
|
||||
sharedUpdater(environment, store, input);
|
||||
},
|
||||
});
|
||||
);
|
||||
createCommentEvent.success({
|
||||
id: result.edge.node.id,
|
||||
status: result.edge.node.status,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
createCommentEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCreateCommentMutation = createMutationContainer(
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import cn from "classnames";
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { OnSubmit } from "coral-framework/lib/form";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import ValidationMessage from "coral-stream/common/ValidationMessage";
|
||||
import { CreateCommentFocusEvent } from "coral-stream/events";
|
||||
import { AriaInfo, Button, Flex, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../../helpers";
|
||||
@@ -35,116 +37,124 @@ interface Props {
|
||||
story: PropTypesOf<typeof MessageBoxContainer>["story"];
|
||||
}
|
||||
|
||||
const PostCommentForm: FunctionComponent<Props> = props => (
|
||||
<div className={CLASSES.createComment.$root}>
|
||||
{props.showMessageBox && (
|
||||
<MessageBoxContainer
|
||||
story={props.story}
|
||||
className={cn(CLASSES.createComment.message, styles.messageBox)}
|
||||
/>
|
||||
)}
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, submitError, form }) => (
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id="comments-postCommentForm-form"
|
||||
>
|
||||
<FormSpy
|
||||
onChange={state => props.onChange && props.onChange(state, form)}
|
||||
/>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
>
|
||||
{/* FIXME: (wyattjoh) reorganize this */}
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
<Localized id="comments-postCommentForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
const PostCommentForm: FunctionComponent<Props> = props => {
|
||||
const emitFocusEvent = useViewerEvent(CreateCommentFocusEvent);
|
||||
const onFocus = useCallback(() => {
|
||||
emitFocusEvent();
|
||||
}, [emitFocusEvent]);
|
||||
return (
|
||||
<div className={CLASSES.createComment.$root}>
|
||||
{props.showMessageBox && (
|
||||
<MessageBoxContainer
|
||||
story={props.story}
|
||||
className={cn(CLASSES.createComment.message, styles.messageBox)}
|
||||
/>
|
||||
)}
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, submitError, form }) => (
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id="comments-postCommentForm-form"
|
||||
>
|
||||
<FormSpy
|
||||
onChange={state => props.onChange && props.onChange(state, form)}
|
||||
/>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="body"
|
||||
validate={getCommentBodyValidators(props.min, props.max)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<>
|
||||
<HorizontalGutter size="half">
|
||||
<Localized id="comments-postCommentForm-rteLabel">
|
||||
<AriaInfo
|
||||
component="label"
|
||||
htmlFor="comments-postCommentForm-field"
|
||||
>
|
||||
Post a comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="comments-postCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
Post a comment
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="comments-postCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTE
|
||||
inputId="comments-postCommentForm-field"
|
||||
onChange={({ html }) =>
|
||||
input.onChange(cleanupRTEEmptyHTML(html))
|
||||
}
|
||||
contentClassName={
|
||||
props.showMessageBox
|
||||
? styles.rteBorderless
|
||||
: undefined
|
||||
}
|
||||
value={input.value}
|
||||
placeholder="Post a comment"
|
||||
disabled={submitting || props.disabled}
|
||||
/>
|
||||
</Localized>
|
||||
{props.disabled ? (
|
||||
<>
|
||||
{props.disabledMessage && (
|
||||
<ValidationMessage fullWidth>
|
||||
{props.disabledMessage}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{meta.touched &&
|
||||
(meta.error ||
|
||||
(meta.submitError &&
|
||||
!meta.dirtySinceLastSubmit)) && (
|
||||
<RTE
|
||||
inputId="comments-postCommentForm-field"
|
||||
onFocus={onFocus}
|
||||
onChange={({ html }) =>
|
||||
input.onChange(cleanupRTEEmptyHTML(html))
|
||||
}
|
||||
contentClassName={
|
||||
props.showMessageBox
|
||||
? styles.rteBorderless
|
||||
: undefined
|
||||
}
|
||||
value={input.value}
|
||||
placeholder="Post a comment"
|
||||
disabled={submitting || props.disabled}
|
||||
/>
|
||||
</Localized>
|
||||
{props.disabled ? (
|
||||
<>
|
||||
{props.disabledMessage && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
{props.disabledMessage}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage fullWidth>
|
||||
{submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
<PostCommentSubmitStatusContainer
|
||||
status={props.submitStatus}
|
||||
/>
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={input.value}
|
||||
max={props.max}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{meta.touched &&
|
||||
(meta.error ||
|
||||
(meta.submitError &&
|
||||
!meta.dirtySinceLastSubmit)) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
{submitError && (
|
||||
<ValidationMessage fullWidth>
|
||||
{submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
<PostCommentSubmitStatusContainer
|
||||
status={props.submitStatus}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<Flex direction="column" alignItems="flex-end">
|
||||
<Localized id="comments-postCommentForm-submit">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
className={CLASSES.createComment.submit}
|
||||
disabled={submitting || !input.value || props.disabled}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
{props.max && (
|
||||
<RemainingCharactersContainer
|
||||
value={input.value}
|
||||
max={props.max}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<Flex direction="column" alignItems="flex-end">
|
||||
<Localized id="comments-postCommentForm-submit">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
className={CLASSES.createComment.submit}
|
||||
disabled={
|
||||
submitting || !input.value || props.disabled
|
||||
}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PostCommentForm;
|
||||
|
||||
@@ -2,8 +2,10 @@ import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { CreateCommentFocusEvent } from "coral-stream/events";
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import RTE from "../../RTE";
|
||||
@@ -20,6 +22,10 @@ interface Props {
|
||||
}
|
||||
|
||||
const PostCommentFormFake: FunctionComponent<Props> = props => {
|
||||
const emitFocusEvent = useViewerEvent(CreateCommentFocusEvent);
|
||||
const onFocus = useCallback(() => {
|
||||
emitFocusEvent();
|
||||
}, [emitFocusEvent]);
|
||||
const onChange = useCallback(
|
||||
(data: { html: string; text: string }) => props.onDraftChange(data.html),
|
||||
[props.onDraftChange]
|
||||
@@ -42,6 +48,7 @@ const PostCommentFormFake: FunctionComponent<Props> = props => {
|
||||
placeholder="Post a comment"
|
||||
value={props.draft}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
</Localized>
|
||||
</div>
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ exports[`renders correctly 1`] = `
|
||||
>
|
||||
<RTE
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder="Post a comment"
|
||||
value=""
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { OpenSortMenuEvent } from "coral-stream/events";
|
||||
import {
|
||||
Flex,
|
||||
Icon,
|
||||
@@ -26,48 +28,55 @@ interface Props {
|
||||
reactionSortLabel: string;
|
||||
}
|
||||
|
||||
const SortMenu: FunctionComponent<Props> = props => (
|
||||
<MatchMedia ltWidth="sm">
|
||||
{matches => (
|
||||
<Flex
|
||||
className={cn(props.className, CLASSES.sortMenu)}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
itemGutter
|
||||
>
|
||||
{!matches && (
|
||||
<Localized id="comments-sortMenu-sortBy">
|
||||
<Typography
|
||||
variant="bodyCopyBold"
|
||||
container={<label htmlFor="coral-comments-sortMenu" />}
|
||||
>
|
||||
Sort By
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<SelectField
|
||||
id="coral-comments-sortMenu"
|
||||
value={props.orderBy}
|
||||
onChange={props.onChange}
|
||||
afterWrapper={(matches && <Icon>sort</Icon>) || undefined}
|
||||
classes={{
|
||||
select: (matches && styles.mobileSelect) || undefined,
|
||||
afterWrapper: (matches && styles.mobileAfterWrapper) || undefined,
|
||||
}}
|
||||
const SortMenu: FunctionComponent<Props> = props => {
|
||||
const emitOpenSortMenuEvent = useViewerEvent(OpenSortMenuEvent);
|
||||
const onClickSelectField = useCallback(() => emitOpenSortMenuEvent(), [
|
||||
emitOpenSortMenuEvent,
|
||||
]);
|
||||
return (
|
||||
<MatchMedia ltWidth="sm">
|
||||
{matches => (
|
||||
<Flex
|
||||
className={cn(props.className, CLASSES.sortMenu)}
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
itemGutter
|
||||
>
|
||||
<Localized id="comments-sortMenu-newest">
|
||||
<Option value="CREATED_AT_DESC">Newest</Option>
|
||||
</Localized>
|
||||
<Localized id="comments-sortMenu-oldest">
|
||||
<Option value="CREATED_AT_ASC">Oldest</Option>
|
||||
</Localized>
|
||||
<Localized id="comments-sortMenu-mostReplies">
|
||||
<Option value="REPLIES_DESC">Most Replies</Option>
|
||||
</Localized>
|
||||
<Option value="REACTION_DESC">{props.reactionSortLabel}</Option>
|
||||
</SelectField>
|
||||
</Flex>
|
||||
)}
|
||||
</MatchMedia>
|
||||
);
|
||||
{!matches && (
|
||||
<Localized id="comments-sortMenu-sortBy">
|
||||
<Typography
|
||||
variant="bodyCopyBold"
|
||||
container={<label htmlFor="coral-comments-sortMenu" />}
|
||||
>
|
||||
Sort By
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<SelectField
|
||||
id="coral-comments-sortMenu"
|
||||
value={props.orderBy}
|
||||
onChange={props.onChange}
|
||||
onClick={onClickSelectField}
|
||||
afterWrapper={(matches && <Icon>sort</Icon>) || undefined}
|
||||
classes={{
|
||||
select: (matches && styles.mobileSelect) || undefined,
|
||||
afterWrapper: (matches && styles.mobileAfterWrapper) || undefined,
|
||||
}}
|
||||
>
|
||||
<Localized id="comments-sortMenu-newest">
|
||||
<Option value="CREATED_AT_DESC">Newest</Option>
|
||||
</Localized>
|
||||
<Localized id="comments-sortMenu-oldest">
|
||||
<Option value="CREATED_AT_ASC">Oldest</Option>
|
||||
</Localized>
|
||||
<Localized id="comments-sortMenu-mostReplies">
|
||||
<Option value="REPLIES_DESC">Most Replies</Option>
|
||||
</Localized>
|
||||
<Option value="REACTION_DESC">{props.reactionSortLabel}</Option>
|
||||
</SelectField>
|
||||
</Flex>
|
||||
)}
|
||||
</MatchMedia>
|
||||
);
|
||||
};
|
||||
export default SortMenu;
|
||||
|
||||
@@ -3,11 +3,16 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useEffect } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { useLocal, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import Counter from "coral-stream/common/Counter";
|
||||
import { UserBoxContainer } from "coral-stream/common/UserBox";
|
||||
import {
|
||||
SetCommentsOrderByEvent,
|
||||
SetCommentsTabEvent,
|
||||
} from "coral-stream/events";
|
||||
import {
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
@@ -71,6 +76,8 @@ const TabWithFeaturedTooltip: FunctionComponent<PropTypesOf<typeof Tab>> = ({
|
||||
);
|
||||
|
||||
export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
const emitSetCommentsTabEvent = useViewerEvent(SetCommentsTabEvent);
|
||||
const emitSetCommentsOrderByEvent = useViewerEvent(SetCommentsOrderByEvent);
|
||||
const [local, setLocal] = useLocal<StreamContainerLocal>(
|
||||
graphql`
|
||||
fragment StreamContainerLocal on Local {
|
||||
@@ -80,13 +87,26 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
`
|
||||
);
|
||||
const onChangeOrder = useCallback(
|
||||
(order: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setLocal({ commentsOrderBy: order.target.value as any }),
|
||||
[setLocal]
|
||||
(order: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (local.commentsOrderBy === order.target.value) {
|
||||
return;
|
||||
}
|
||||
setLocal({ commentsOrderBy: order.target.value as any });
|
||||
emitSetCommentsOrderByEvent({ orderBy: order.target.value });
|
||||
},
|
||||
[setLocal, local.commentsOrderBy]
|
||||
);
|
||||
const onChangeTab = useCallback(
|
||||
(tab: COMMENTS_TAB) => setLocal({ commentsTab: tab }),
|
||||
[setLocal]
|
||||
(tab: COMMENTS_TAB, emit = true) => {
|
||||
if (local.commentsTab === tab) {
|
||||
return;
|
||||
}
|
||||
setLocal({ commentsTab: tab });
|
||||
if (emit) {
|
||||
emitSetCommentsTabEvent({ tab });
|
||||
}
|
||||
},
|
||||
[setLocal, local.commentsTab]
|
||||
);
|
||||
const banned = Boolean(
|
||||
props.viewer && props.viewer.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
@@ -110,9 +130,9 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
// If the selected tab is FEATURED_COMMENTS, but there aren't any featured
|
||||
// comments, then switch it to the all comments tab.
|
||||
if (featuredCommentsCount === 0) {
|
||||
onChangeTab("ALL_COMMENTS");
|
||||
onChangeTab("ALL_COMMENTS", false);
|
||||
} else {
|
||||
onChangeTab("FEATURED_COMMENTS");
|
||||
onChangeTab("FEATURED_COMMENTS", false);
|
||||
}
|
||||
}
|
||||
}, [featuredCommentsCount, local.commentsTab, onChangeTab]);
|
||||
|
||||
@@ -18,6 +18,7 @@ exports[`renders correctly on big screens 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_ASC"
|
||||
>
|
||||
@@ -69,6 +70,7 @@ exports[`renders correctly on small screens 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_ASC"
|
||||
>
|
||||
|
||||
+39
-9
@@ -1,6 +1,7 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { UpdateStorySettingsMutation as MutationTypes } from "coral-stream/__generated__/UpdateStorySettingsMutation.graphql";
|
||||
import { UpdateStorySettingsEvent } from "coral-stream/events";
|
||||
|
||||
export type UpdateStorySettingsInput = MutationInput<MutationTypes>;
|
||||
|
||||
@@ -25,16 +27,44 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: UpdateStorySettingsInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: UpdateStorySettingsInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) {
|
||||
const updateStorySettings = UpdateStorySettingsEvent.begin(eventEmitter, {
|
||||
storyID: input.id,
|
||||
live: input.settings.live ? { enabled: input.settings.live.enabled } : null,
|
||||
messageBox: input.settings.messageBox
|
||||
? {
|
||||
content: input.settings.messageBox.content,
|
||||
enabled: input.settings.messageBox.enabled,
|
||||
icon: input.settings.messageBox.icon,
|
||||
}
|
||||
: null,
|
||||
moderation: input.settings.moderation,
|
||||
premodLinksEnable: input.settings.premodLinksEnable,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: input.id,
|
||||
settings: input.settings,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
updateStorySettings.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
updateStorySettings.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withUpdateStorySettingsMutation = createMutationContainer(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { CloseStoryEvent } from "coral-stream/events";
|
||||
|
||||
import { CloseStoryMutation as MutationTypes } from "coral-stream/__generated__/CloseStoryMutation.graphql";
|
||||
|
||||
@@ -25,16 +27,33 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CloseStoryInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: CloseStoryInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) {
|
||||
const closeStoryEvent = CloseStoryEvent.begin(eventEmitter, {
|
||||
storyID: input.id,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
closeStoryEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
closeStoryEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCloseStoryMutation = createMutationContainer(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { OpenStoryEvent } from "coral-stream/events";
|
||||
|
||||
import { OpenStoryMutation as MutationTypes } from "coral-stream/__generated__/OpenStoryMutation.graphql";
|
||||
|
||||
@@ -25,16 +27,33 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: OpenStoryInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
async function commit(
|
||||
environment: Environment,
|
||||
input: OpenStoryInput,
|
||||
{ eventEmitter }: CoralContext
|
||||
) {
|
||||
const openStoryEvent = OpenStoryEvent.begin(eventEmitter, {
|
||||
storyID: input.id,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: input.id,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
openStoryEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
openStoryEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const withOpenStoryMutation = createMutationContainer(
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
import { useViewerNetworkEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { LoadMoreHistoryCommentsEvent } from "coral-stream/events";
|
||||
|
||||
import { CommentHistoryContainer_settings as SettingsData } from "coral-stream/__generated__/CommentHistoryContainer_settings.graphql";
|
||||
import { CommentHistoryContainer_story as StoryData } from "coral-stream/__generated__/CommentHistoryContainer_story.graphql";
|
||||
@@ -10,57 +15,47 @@ import { CommentHistoryContainerPaginationQueryVariables } from "coral-stream/__
|
||||
|
||||
import CommentHistory from "./CommentHistory";
|
||||
|
||||
interface CommentHistoryContainerProps {
|
||||
interface Props {
|
||||
viewer: ViewerData;
|
||||
story: StoryData;
|
||||
settings: SettingsData;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
export class CommentHistoryContainer extends React.Component<
|
||||
CommentHistoryContainerProps
|
||||
> {
|
||||
public state = {
|
||||
disableLoadMore: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const comments = this.props.viewer.comments.edges.map(edge => edge.node);
|
||||
return (
|
||||
<CommentHistory
|
||||
story={this.props.story}
|
||||
settings={this.props.settings}
|
||||
comments={comments}
|
||||
onLoadMore={this.loadMore}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableLoadMore={this.state.disableLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private loadMore = () => {
|
||||
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
|
||||
return;
|
||||
export const CommentHistoryContainer: FunctionComponent<Props> = props => {
|
||||
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||
const beginLoadMoreEvent = useViewerNetworkEvent(
|
||||
LoadMoreHistoryCommentsEvent
|
||||
);
|
||||
const loadMoreAndEmit = useCallback(async () => {
|
||||
const loadMoreEvent = beginLoadMoreEvent();
|
||||
try {
|
||||
await loadMore();
|
||||
loadMoreEvent.success();
|
||||
} catch (error) {
|
||||
loadMoreEvent.error({ message: error.message, code: error.code });
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
this.setState({ disableLoadMore: true });
|
||||
this.props.relay.loadMore(
|
||||
10, // Fetch the next 10 feed items
|
||||
error => {
|
||||
this.setState({ disableLoadMore: false });
|
||||
if (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
}, [loadMore, beginLoadMoreEvent]);
|
||||
const comments = props.viewer.comments.edges.map(edge => edge.node);
|
||||
return (
|
||||
<CommentHistory
|
||||
story={props.story}
|
||||
settings={props.settings}
|
||||
comments={comments}
|
||||
onLoadMore={loadMoreAndEmit}
|
||||
hasMore={props.relay.hasMore()}
|
||||
disableLoadMore={isLoadingMore}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: (cvle) If this could be autogenerated.
|
||||
type FragmentVariables = CommentHistoryContainerPaginationQueryVariables;
|
||||
|
||||
const enhanced = withPaginationContainer<
|
||||
CommentHistoryContainerProps,
|
||||
Props,
|
||||
CommentHistoryContainerPaginationQueryVariables,
|
||||
FragmentVariables
|
||||
>(
|
||||
|
||||
@@ -4,13 +4,13 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import HTMLContent from "coral-stream/common/HTMLContent";
|
||||
import Timestamp from "coral-stream/common/Timestamp";
|
||||
import InReplyTo from "coral-stream/tabs/Comments/Comment/InReplyTo";
|
||||
import {
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
TextLink,
|
||||
Timestamp,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { getURLWithCommentID } from "coral-framework/helpers";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { ViewConversationEvent } from "coral-stream/events";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
@@ -14,45 +16,50 @@ import { HistoryCommentContainer_story as StoryData } from "coral-stream/__gener
|
||||
|
||||
import HistoryComment from "./HistoryComment";
|
||||
|
||||
interface HistoryCommentContainerProps {
|
||||
interface Props {
|
||||
setCommentID: SetCommentIDMutation;
|
||||
story: StoryData;
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
export class HistoryCommentContainer extends React.Component<
|
||||
HistoryCommentContainerProps
|
||||
> {
|
||||
private handleGoToConversation = (e: React.MouseEvent) => {
|
||||
if (this.props.story.id === this.props.comment.story.id) {
|
||||
this.props.setCommentID({ id: this.props.comment.id });
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
public render() {
|
||||
return (
|
||||
<HistoryComment
|
||||
{...this.props.comment}
|
||||
reactionCount={this.props.comment.actionCounts.reaction.total}
|
||||
reactionSettings={this.props.settings.reaction}
|
||||
parentAuthorName={
|
||||
this.props.comment.parent &&
|
||||
this.props.comment.parent.author &&
|
||||
this.props.comment.parent.author.username
|
||||
}
|
||||
conversationURL={getURLWithCommentID(
|
||||
this.props.comment.story.url,
|
||||
this.props.comment.id
|
||||
)}
|
||||
onGotoConversation={this.handleGoToConversation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const HistoryCommentContainer: FunctionComponent<Props> = props => {
|
||||
const emitViewConversationEvent = useViewerEvent(ViewConversationEvent);
|
||||
const handleGotoConversation = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (props.story.id === props.comment.story.id) {
|
||||
props.setCommentID({ id: props.comment.id });
|
||||
emitViewConversationEvent({
|
||||
from: "COMMENT_HISTORY",
|
||||
commentID: props.comment.id,
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[props.story.id, props.comment.story.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<HistoryComment
|
||||
{...props.comment}
|
||||
reactionCount={props.comment.actionCounts.reaction.total}
|
||||
reactionSettings={props.settings.reaction}
|
||||
parentAuthorName={
|
||||
props.comment.parent &&
|
||||
props.comment.parent.author &&
|
||||
props.comment.parent.author.username
|
||||
}
|
||||
conversationURL={getURLWithCommentID(
|
||||
props.comment.story.url,
|
||||
props.comment.id
|
||||
)}
|
||||
onGotoConversation={handleGotoConversation}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withSetCommentIDMutation(
|
||||
withFragmentContainer<HistoryCommentContainerProps>({
|
||||
withFragmentContainer<Props>({
|
||||
story: graphql`
|
||||
fragment HistoryCommentContainer_story on Story {
|
||||
id
|
||||
|
||||
+2
-3
@@ -25,12 +25,11 @@ exports[`renders correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</div>
|
||||
<div>
|
||||
<Timestamp
|
||||
<TimeStamp
|
||||
className="coral coral-myComment-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</Timestamp>
|
||||
</TimeStamp>
|
||||
<ForwardRef(forwardRef)
|
||||
container="div"
|
||||
variant="bodyCopy"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { graphql, useLocal } from "coral-framework/lib/relay";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import UserBoxContainer from "coral-stream/common/UserBox";
|
||||
import { SetProfileTabEvent } from "coral-stream/events";
|
||||
import {
|
||||
HorizontalGutter,
|
||||
Tab,
|
||||
@@ -35,14 +37,20 @@ export interface ProfileProps {
|
||||
}
|
||||
|
||||
const Profile: FunctionComponent<ProfileProps> = props => {
|
||||
const emitSetProfileTabEvent = useViewerEvent(SetProfileTabEvent);
|
||||
const [local, setLocal] = useLocal<ProfileLocal>(graphql`
|
||||
fragment ProfileLocal on Local {
|
||||
profileTab
|
||||
}
|
||||
`);
|
||||
const onTabClick = useCallback(
|
||||
(tab: ProfileLocal["profileTab"]) => setLocal({ profileTab: tab }),
|
||||
[setLocal]
|
||||
(tab: ProfileLocal["profileTab"]) => {
|
||||
if (local.profileTab !== tab) {
|
||||
emitSetProfileTabEvent({ tab });
|
||||
setLocal({ profileTab: tab });
|
||||
}
|
||||
},
|
||||
[setLocal, local.profileTab]
|
||||
);
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Environment } from "relay-runtime";
|
||||
import { PasswordField } from "coral-framework/components";
|
||||
import getAuthenticationIntegrations from "coral-framework/helpers/getAuthenticationIntegrations";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { colorFromMeta } from "coral-framework/lib/form";
|
||||
import {
|
||||
createFetch,
|
||||
@@ -28,6 +29,10 @@ import {
|
||||
} from "coral-framework/lib/validation";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import FieldValidationMessage from "coral-stream/common/FieldValidationMessage";
|
||||
import {
|
||||
ResendEmailVerificationEvent,
|
||||
ShowEditEmailDialogEvent,
|
||||
} from "coral-stream/events";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
@@ -50,10 +55,23 @@ import styles from "./ChangeEmailContainer.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"resendConfirmation",
|
||||
(environment: Environment, variables, context) => {
|
||||
return context.rest.fetch<void>("/account/confirm", {
|
||||
method: "POST",
|
||||
});
|
||||
async (environment: Environment, variables, { eventEmitter, rest }) => {
|
||||
const resendEmailVerificationEvent = ResendEmailVerificationEvent.begin(
|
||||
eventEmitter
|
||||
);
|
||||
try {
|
||||
const result = await rest.fetch<void>("/account/confirm", {
|
||||
method: "POST",
|
||||
});
|
||||
resendEmailVerificationEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
resendEmailVerificationEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -71,6 +89,7 @@ const changeEmailContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
}) => {
|
||||
const emitShowEvent = useViewerEvent(ShowEditEmailDialogEvent);
|
||||
const updateEmail = useMutation(UpdateEmailMutation);
|
||||
|
||||
const [showEditForm, setShowEditForm] = useState(false);
|
||||
@@ -82,6 +101,9 @@ const changeEmailContainer: FunctionComponent<Props> = ({
|
||||
}, [fetcher]);
|
||||
|
||||
const toggleEditForm = useCallback(() => {
|
||||
if (!showEditForm) {
|
||||
emitShowEvent();
|
||||
}
|
||||
setShowEditForm(!showEditForm);
|
||||
}, [setShowEditForm, showEditForm]);
|
||||
const onSubmit = useCallback(
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ChangeEmailEvent } from "coral-stream/events";
|
||||
|
||||
import { UpdateEmailMutation as MutationTypes } from "coral-stream/__generated__/UpdateEmailMutation.graphql";
|
||||
|
||||
@@ -14,40 +15,59 @@ let clientMutationId = 0;
|
||||
|
||||
const UpdateEmailMutation = createMutation(
|
||||
"updateEmail",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateEmailMutation($input: UpdateEmailInput!) {
|
||||
updateEmail(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
id
|
||||
email
|
||||
emailVerified
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const changeEmailEvent = ChangeEmailEvent.begin(eventEmitter, {
|
||||
oldEmail: getViewer(environment)!.email!,
|
||||
newEmail: input.email,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation UpdateEmailMutation($input: UpdateEmailInput!) {
|
||||
updateEmail(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
id
|
||||
email
|
||||
emailVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateEmail: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
user: {
|
||||
// Only a logged in user will be able to change its email
|
||||
// and access this mutation, so the viewer is always available
|
||||
// in the cache when calling this mutation.
|
||||
id: getViewer(environment)!.id,
|
||||
email: input.email,
|
||||
emailVerified: false,
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
optimisticResponse: {
|
||||
updateEmail: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
user: {
|
||||
// Only a logged in user will be able to change its email
|
||||
// and access this mutation, so the viewer is always available
|
||||
// in the cache when calling this mutation.
|
||||
id: getViewer(environment)!.id,
|
||||
email: input.email,
|
||||
emailVerified: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
changeEmailEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
changeEmailEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default UpdateEmailMutation;
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { colorFromMeta } from "coral-framework/lib/form";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "coral-framework/lib/validation";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import FieldValidationMessage from "coral-stream/common/FieldValidationMessage";
|
||||
import { ShowEditPasswordDialogEvent } from "coral-stream/events";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
@@ -40,6 +42,7 @@ interface FormProps {
|
||||
}
|
||||
|
||||
const ChangePassword: FunctionComponent<Props> = ({ onResetPassword }) => {
|
||||
const emitShowEvent = useViewerEvent(ShowEditPasswordDialogEvent);
|
||||
const updatePassword = useMutation(UpdatePasswordMutation);
|
||||
const onSubmit = useCallback(
|
||||
async (input: FormProps, form: FormApi) => {
|
||||
@@ -64,10 +67,12 @@ const ChangePassword: FunctionComponent<Props> = ({ onResetPassword }) => {
|
||||
);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const toggleForm = useCallback(() => setShowForm(!showForm), [
|
||||
showForm,
|
||||
setShowForm,
|
||||
]);
|
||||
const toggleForm = useCallback(() => {
|
||||
if (!showForm) {
|
||||
emitShowEvent();
|
||||
}
|
||||
setShowForm(!showForm);
|
||||
}, [showForm, setShowForm]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
+8
@@ -14,6 +14,7 @@ import { reduceSeconds, UNIT } from "coral-common/helpers/i18n";
|
||||
import getAuthenticationIntegrations from "coral-framework/helpers/getAuthenticationIntegrations";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from "coral-framework/lib/validation";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import FieldValidationMessage from "coral-stream/common/FieldValidationMessage";
|
||||
import { ShowEditUsernameDialogEvent } from "coral-stream/events";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -66,9 +68,15 @@ const ChangeUsernameContainer: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
}) => {
|
||||
const emitShowEditUsernameDialog = useViewerEvent(
|
||||
ShowEditUsernameDialogEvent
|
||||
);
|
||||
const [showEditForm, setShowEditForm] = useState(false);
|
||||
const [showSuccessMessage, setShowSuccessMessage] = useState(false);
|
||||
const toggleEditForm = useCallback(() => {
|
||||
if (!showEditForm) {
|
||||
emitShowEditUsernameDialog();
|
||||
}
|
||||
setShowEditForm(!showEditForm);
|
||||
}, [setShowEditForm, showEditForm]);
|
||||
const updateUsername = useMutation(UpdateUsernameMutation);
|
||||
|
||||
+59
-39
@@ -9,58 +9,78 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { UpdateUsernameMutation as MutationTypes } from "coral-stream/__generated__/UpdateUsernameMutation.graphql";
|
||||
import { ChangeUsernameEvent } from "coral-stream/events";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const UpdateUsernameMutation = createMutation(
|
||||
"updateUsername",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateUsernameMutation($input: UpdateUsernameInput!) {
|
||||
updateUsername(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
username
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
username
|
||||
createdAt
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const changeUsernameEvent = ChangeUsernameEvent.begin(eventEmitter, {
|
||||
oldUsername: getViewer(environment)!.username!,
|
||||
newUsername: input.username,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation UpdateUsernameMutation($input: UpdateUsernameInput!) {
|
||||
updateUsername(input: $input) {
|
||||
clientMutationId
|
||||
user {
|
||||
username
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
username
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateUsername: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
user: {
|
||||
id: getViewer(environment)!.id,
|
||||
username: input.username,
|
||||
status: {
|
||||
username: {
|
||||
// FIXME: (tessalt) merge in existing history
|
||||
history: [
|
||||
{
|
||||
username: input.username,
|
||||
createdAt: Date.now(),
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateUsername: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
user: {
|
||||
id: getViewer(environment)!.id,
|
||||
username: input.username,
|
||||
status: {
|
||||
username: {
|
||||
// FIXME: (tessalt) merge in existing history
|
||||
history: [
|
||||
{
|
||||
username: input.username,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
);
|
||||
changeUsernameEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
changeUsernameEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default UpdateUsernameMutation;
|
||||
|
||||
+51
-31
@@ -9,6 +9,7 @@ import {
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { RequestAccountDeletionEvent } from "coral-stream/events";
|
||||
|
||||
import { RequestAccountDeletionMutation as MutationTypes } from "coral-stream/__generated__/RequestAccountDeletionMutation.graphql";
|
||||
|
||||
@@ -16,39 +17,58 @@ let clientMutationId = 0;
|
||||
|
||||
const RequestAccountDeletionMutation = createMutation(
|
||||
"requestAccountDeletion",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RequestAccountDeletionMutation(
|
||||
$input: RequestAccountDeletionInput!
|
||||
) {
|
||||
requestAccountDeletion(input: $input) {
|
||||
user {
|
||||
scheduledDeletionDate
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const requestAccountDeletionEvent = RequestAccountDeletionEvent.begin(
|
||||
eventEmitter
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation RequestAccountDeletionMutation(
|
||||
$input: RequestAccountDeletionInput!
|
||||
) {
|
||||
requestAccountDeletion(input: $input) {
|
||||
user {
|
||||
scheduledDeletionDate
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const deletionDate = DateTime.fromJSDate(new Date())
|
||||
.plus({ days: SCHEDULED_DELETION_TIMESPAN_DAYS })
|
||||
.toISO();
|
||||
const viewerProxy = store.get(viewer.id);
|
||||
if (viewerProxy !== null) {
|
||||
viewerProxy.setValue(deletionDate, "scheduledDeletionDate");
|
||||
}
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
|
||||
const deletionDate = DateTime.fromJSDate(new Date())
|
||||
.plus({ days: SCHEDULED_DELETION_TIMESPAN_DAYS })
|
||||
.toISO();
|
||||
|
||||
const viewerProxy = store.get(viewer.id);
|
||||
if (viewerProxy !== null) {
|
||||
viewerProxy.setValue(deletionDate, "scheduledDeletionDate");
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
requestAccountDeletionEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
requestAccountDeletionEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default RequestAccountDeletionMutation;
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { useViewerEvent } from "coral-framework/lib/events";
|
||||
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { ShowIgnoreUserdDialogEvent } from "coral-stream/events";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
@@ -24,12 +26,15 @@ interface Props {
|
||||
}
|
||||
|
||||
const IgnoreUserSettingsContainer: FunctionComponent<Props> = ({ viewer }) => {
|
||||
const emitShow = useViewerEvent(ShowIgnoreUserdDialogEvent);
|
||||
const removeUserIgnore = useMutation(RemoveUserIgnoreMutation);
|
||||
const [showManage, setShowManage] = useState(false);
|
||||
const toggleManage = useCallback(() => setShowManage(!showManage), [
|
||||
showManage,
|
||||
setShowManage,
|
||||
]);
|
||||
const toggleManage = useCallback(() => {
|
||||
if (!showManage) {
|
||||
emitShow();
|
||||
}
|
||||
setShowManage(!showManage);
|
||||
}, [showManage, setShowManage]);
|
||||
return (
|
||||
<div
|
||||
data-testid="profile-account-ignoredCommenters"
|
||||
|
||||
@@ -2,11 +2,13 @@ import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { getViewer } from "coral-framework/helpers";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { RemoveUserIgnoreEvent } from "coral-stream/events";
|
||||
|
||||
import { RemoveUserIgnoreMutation as MutationTypes } from "coral-stream/__generated__/RemoveUserIgnoreMutation.graphql";
|
||||
|
||||
@@ -14,37 +16,55 @@ let clientMutationId = 0;
|
||||
|
||||
const RemoveUserIgnoreMutation = createMutation(
|
||||
"removeUserIgnore",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RemoveUserIgnoreMutation($input: RemoveUserIgnoreInput!) {
|
||||
removeUserIgnore(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const removeUserIgnore = RemoveUserIgnoreEvent.begin(eventEmitter, {
|
||||
userID: input.userID,
|
||||
});
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation RemoveUserIgnoreMutation($input: RemoveUserIgnoreInput!) {
|
||||
removeUserIgnore(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const viewerProxy = store.get(viewer.id)!;
|
||||
const removeIgnoredUserRecords = viewerProxy.getLinkedRecords(
|
||||
"ignoredUsers"
|
||||
);
|
||||
if (removeIgnoredUserRecords) {
|
||||
viewerProxy.setLinkedRecords(
|
||||
removeIgnoredUserRecords.filter(
|
||||
r => r!.getValue("id") !== input.userID
|
||||
),
|
||||
"ignoredUsers"
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const viewerProxy = store.get(viewer.id)!;
|
||||
const removeIgnoredUserRecords = viewerProxy.getLinkedRecords(
|
||||
"ignoredUsers"
|
||||
);
|
||||
if (removeIgnoredUserRecords) {
|
||||
viewerProxy.setLinkedRecords(
|
||||
removeIgnoredUserRecords.filter(
|
||||
r => r!.getValue("id") !== input.userID
|
||||
),
|
||||
"ignoredUsers"
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
removeUserIgnore.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
removeUserIgnore.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default RemoveUserIgnoreMutation;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { RequestDownloadCommentHistoryEvent } from "coral-stream/events";
|
||||
|
||||
import { RequestCommentsDownloadMutation as MutationTypes } from "coral-stream/__generated__/RequestCommentsDownloadMutation.graphql";
|
||||
|
||||
@@ -14,7 +15,11 @@ let clientMutationId = 0;
|
||||
|
||||
const RequestCommentsDownloadMutation = createMutation(
|
||||
"requestCommentsDownload",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const updater = (store: RecordSourceSelectorProxy) => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const user = store.get(viewer.id);
|
||||
@@ -24,26 +29,41 @@ const RequestCommentsDownloadMutation = createMutation(
|
||||
user.setValue(now.toISOString(), "lastDownloadedAt");
|
||||
}
|
||||
};
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RequestCommentsDownloadMutation(
|
||||
$input: RequestCommentsDownloadInput!
|
||||
) {
|
||||
requestCommentsDownload(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
const requestDownloadCommentHistoryEvent = RequestDownloadCommentHistoryEvent.begin(
|
||||
eventEmitter
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation RequestCommentsDownloadMutation(
|
||||
$input: RequestCommentsDownloadInput!
|
||||
) {
|
||||
requestCommentsDownload(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: updater,
|
||||
updater,
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
optimisticUpdater: updater,
|
||||
updater,
|
||||
});
|
||||
);
|
||||
requestDownloadCommentHistoryEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
requestDownloadCommentHistoryEvent.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+55
-25
@@ -1,11 +1,13 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { UpdateNotificationSettingsEvent } from "coral-stream/events";
|
||||
|
||||
import { UpdateNotificationSettingsMutation as MutationTypes } from "coral-stream/__generated__/UpdateNotificationSettingsMutation.graphql";
|
||||
|
||||
@@ -13,34 +15,62 @@ let clientMutationId = 0;
|
||||
|
||||
const UpdateNotificationSettingsMutation = createMutation(
|
||||
"updateNotificationSettings",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateNotificationSettingsMutation(
|
||||
$input: UpdateNotificationSettingsInput!
|
||||
) {
|
||||
updateNotificationSettings(input: $input) {
|
||||
user {
|
||||
id
|
||||
notifications {
|
||||
onReply
|
||||
onFeatured
|
||||
onStaffReplies
|
||||
onModeration
|
||||
digestFrequency
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }: CoralContext
|
||||
) => {
|
||||
const updateNofitificationSettings = UpdateNotificationSettingsEvent.begin(
|
||||
eventEmitter,
|
||||
{
|
||||
digestFrequency: input.digestFrequency,
|
||||
onFeatured: input.onFeatured,
|
||||
onModeration: input.onModeration,
|
||||
onStaffReplies: input.onStaffReplies,
|
||||
onReply: input.onReply,
|
||||
}
|
||||
);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation UpdateNotificationSettingsMutation(
|
||||
$input: UpdateNotificationSettingsInput!
|
||||
) {
|
||||
updateNotificationSettings(input: $input) {
|
||||
user {
|
||||
id
|
||||
notifications {
|
||||
onReply
|
||||
onFeatured
|
||||
onStaffReplies
|
||||
onModeration
|
||||
digestFrequency
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
updateNofitificationSettings.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
updateNofitificationSettings.error({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default UpdateNotificationSettingsMutation;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { ChangePasswordEvent } from "coral-stream/events";
|
||||
|
||||
import { UpdatePasswordMutation as MutationTypes } from "coral-stream/__generated__/UpdatePasswordMutation.graphql";
|
||||
|
||||
@@ -13,22 +14,38 @@ let clientMutationId = 0;
|
||||
|
||||
const UpdatePasswordMutation = createMutation(
|
||||
"updatePassword",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdatePasswordMutation($input: UpdatePasswordInput!) {
|
||||
updatePassword(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
async (
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ eventEmitter }
|
||||
) => {
|
||||
const changePasswordEvent = ChangePasswordEvent.begin(eventEmitter);
|
||||
try {
|
||||
const result = await commitMutationPromiseNormalized<MutationTypes>(
|
||||
environment,
|
||||
{
|
||||
mutation: graphql`
|
||||
mutation UpdatePasswordMutation($input: UpdatePasswordInput!) {
|
||||
updatePassword(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
changePasswordEvent.success();
|
||||
return result;
|
||||
} catch (error) {
|
||||
changePasswordEvent.error({ message: error.message, code: error.code });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default UpdatePasswordMutation;
|
||||
|
||||
+3
@@ -86,6 +86,8 @@ exports[`renders comment stream 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -200,6 +202,7 @@ exports[`renders comment stream 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
|
||||
@@ -299,6 +299,8 @@ exports[`edit a comment and handle server error: edit form 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -487,6 +489,8 @@ exports[`edit a comment: edit form 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -675,6 +679,8 @@ exports[`edit a comment: optimistic response 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar "
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -1604,6 +1610,8 @@ exports[`shows expiry message: edit time expired 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar "
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
|
||||
@@ -314,6 +314,8 @@ exports[`post a reply: open reply form 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
|
||||
@@ -270,6 +270,8 @@ exports[`post a reply: open reply form 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
|
||||
+3
@@ -70,6 +70,8 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -184,6 +186,7 @@ exports[`renders comment stream with community guidelines 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
|
||||
+8
@@ -105,6 +105,7 @@ exports[`renders message box when commenting disabled 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
@@ -303,6 +304,8 @@ exports[`renders message box when logged in 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -424,6 +427,7 @@ exports[`renders message box when logged in 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
@@ -603,6 +607,8 @@ exports[`renders message box when not logged in 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -717,6 +723,7 @@ exports[`renders message box when not logged in 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
@@ -917,6 +924,7 @@ exports[`renders message box when story isClosed 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
|
||||
@@ -86,6 +86,8 @@ exports[`renders comment stream 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="$root content placeholder toolbar"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
<div
|
||||
className="RTE-contentEditableContainer"
|
||||
@@ -200,6 +202,7 @@ exports[`renders comment stream 1`] = `
|
||||
id="coral-comments-sortMenu"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
value="CREATED_AT_DESC"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
EventHandler,
|
||||
FocusEvent,
|
||||
FunctionComponent,
|
||||
MouseEvent,
|
||||
} from "react";
|
||||
|
||||
import { withKeyboardFocus, withStyles } from "coral-ui/hocs";
|
||||
@@ -37,6 +38,7 @@ export interface SelectFieldProps {
|
||||
autofocus?: boolean;
|
||||
name?: string;
|
||||
onChange?: EventHandler<ChangeEvent<HTMLSelectElement>>;
|
||||
onClick?: EventHandler<MouseEvent>;
|
||||
disabled?: boolean;
|
||||
|
||||
// These handlers are passed down by the `withKeyboardFocus` HOC.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import React, {
|
||||
EventHandler,
|
||||
FunctionComponent,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { AbsoluteTime, BaseButton, RelativeTime } from "coral-ui/components";
|
||||
|
||||
@@ -9,17 +15,28 @@ export interface TimestampProps {
|
||||
className?: string;
|
||||
children: string;
|
||||
toggleAbsolute?: boolean;
|
||||
onToggleAbsolute?: (absolute: boolean) => void;
|
||||
onClick?: EventHandler<MouseEvent>;
|
||||
}
|
||||
|
||||
const Timestamp: FunctionComponent<TimestampProps> = props => {
|
||||
const [showAbsolute, setShowAbsolute] = useState(false);
|
||||
const toggleShowAbsolute = useCallback(() => {
|
||||
if (props.toggleAbsolute) {
|
||||
setShowAbsolute(!showAbsolute);
|
||||
}
|
||||
}, [showAbsolute, setShowAbsolute]);
|
||||
const handleOnClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (props.toggleAbsolute) {
|
||||
if (props.onToggleAbsolute) {
|
||||
props.onToggleAbsolute(!showAbsolute);
|
||||
}
|
||||
setShowAbsolute(!showAbsolute);
|
||||
}
|
||||
if (props.onClick) {
|
||||
return props.onClick(event);
|
||||
}
|
||||
},
|
||||
[showAbsolute, setShowAbsolute, props.onClick]
|
||||
);
|
||||
return (
|
||||
<BaseButton className={styles.root} onClick={toggleShowAbsolute}>
|
||||
<BaseButton className={styles.root} onClick={handleOnClick}>
|
||||
{showAbsolute ? (
|
||||
<AbsoluteTime
|
||||
date={props.children}
|
||||
|
||||
Reference in New Issue
Block a user