[CORL-1248] Allow users to auto-show media in stream (#3086)

* Create preliminary media preferences

Allows user to select whether they want to
automatically show media or not.

CORL-1248

* Fix up the copy around media preferences

CORL-1248

* Rename EmbedPreferences to UserMediaSettings

CORL-1248

* Add error/validation messages to media prefs

CORL-1248

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Nick Funk
2020-08-10 14:12:56 -06:00
committed by GitHub
parent adc508c7dc
commit 45cd0e6628
17 changed files with 423 additions and 4 deletions
+12
View File
@@ -133,6 +133,7 @@ createComment.error
- <a href="#unfeatureComment">unfeatureComment</a>
- <a href="#updateNotificationSettings">updateNotificationSettings</a>
- <a href="#updateStorySettings">updateStorySettings</a>
- <a href="#updateUserMediaSettings">updateUserMediaSettings</a>
- <a href="#viewConversation">viewConversation</a>
- <a href="#viewFullDiscussion">viewFullDiscussion</a>
- <a href="#viewNewComments">viewNewComments</a>
@@ -587,6 +588,17 @@ createComment.error
};
}
```
- <a id="updateUserMediaSettings">**updateUserMediaSettings.success**, **updateUserMediaSettings.error**</a>:
```ts
{
unfurlEmbeds?: boolean | 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
{
+4
View File
@@ -968,6 +968,10 @@ const CLASSES = {
updateButton: "coral coral-emailNotifications-updateButton",
},
mediaPreferences: {
updateButton: "coral coral-mediaPreferences-updateButton",
},
/**
* spinner is the loading indicator.
*/
+9
View File
@@ -217,6 +217,15 @@ export const UpdateNotificationSettingsEvent = createViewerNetworkEvent<{
};
}>("updateNotificationSettings");
export const UpdateUserMediaSettingsEvent = createViewerNetworkEvent<{
unfurlEmbeds?: boolean | null;
success: {};
error: {
message: string;
code?: string;
};
}>("updateUserMediaSettings");
/**
* This event is emitted when the viewer updates the story settings.
*/
@@ -409,7 +409,11 @@ export const CommentContainer: FunctionComponent<Props> = ({
</Flex>
}
media={
<MediaSectionContainer comment={comment} settings={settings} />
<MediaSectionContainer
comment={comment}
settings={settings}
defaultExpanded={viewer?.mediaSettings?.unfurlEmbeds}
/>
}
footer={
<>
@@ -533,6 +537,9 @@ const enhanced = withContext(({ eventEmitter }) => ({ eventEmitter }))(
badges
role
scheduledDeletionDate
mediaSettings {
unfurlEmbeds
}
...UsernameWithPopoverContainer_viewer
...ReactionButtonContainer_viewer
...ReportFlowContainer_viewer
@@ -18,14 +18,16 @@ import styles from "./MediaSectionContainer.css";
interface Props {
comment: MediaSectionContainer_comment;
settings: MediaSectionContainer_settings;
defaultExpanded: boolean | null | undefined;
}
const MediaSectionContainer: FunctionComponent<Props> = ({
comment,
settings,
defaultExpanded,
}) => {
const { revision } = comment;
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(defaultExpanded ? true : false);
const onToggleExpand = useCallback(() => {
setExpanded((v) => !v);
}, []);
@@ -68,7 +68,11 @@ const FeaturedCommentContainer: FunctionComponent<Props> = (props) => {
>
{comment.body || ""}
</HTMLContent>
<MediaSectionContainer comment={comment} settings={settings} />
<MediaSectionContainer
comment={comment}
settings={settings}
defaultExpanded={viewer?.mediaSettings?.unfurlEmbeds}
/>
</HorizontalGutter>
<Flex
direction="row"
@@ -165,6 +169,9 @@ const enhanced = withSetCommentIDMutation(
ignoredUsers {
id
}
mediaSettings {
unfurlEmbeds
}
role
...UsernameWithPopoverContainer_viewer
...ReactionButtonContainer_viewer
@@ -59,6 +59,7 @@ const HistoryCommentContainer: FunctionComponent<Props> = (props) => {
<MediaSectionContainer
comment={props.comment}
settings={props.settings}
defaultExpanded={false}
/>
}
/>
@@ -0,0 +1,42 @@
.form {
padding-bottom: var(--spacing-1);
}
.title {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-primary-semi-bold);
font-size: var(--font-size-4);
line-height: 1.11;
color: var(--palette-text-900);
padding-bottom: var(--spacing-4);
}
.options {
padding-bottom: var(--spacing-3);
}
.checkBoxDescription {
font-family: var(--font-family-primary);
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-2);
color: var(--palette-grey-500);
padding-left: calc(var(--spacing-3) + 14px);
}
.updateButton {
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-1);
}
.updateButtonNotification {
padding-top: var(--spacing-2);
padding-bottom: var(--spacing-3);
}
.callOut {
padding-bottom: var(--spacing-2);
}
@@ -0,0 +1,156 @@
import { Localized } from "@fluent/react/compat";
import cn from "classnames";
import { FORM_ERROR } from "final-form";
import React, { FunctionComponent, useCallback, useState } from "react";
import { Field, Form } from "react-final-form";
import { graphql } from "react-relay";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
import CLASSES from "coral-stream/classes";
import {
CheckBox,
FieldSet,
FormField,
HorizontalGutter,
Icon,
} from "coral-ui/components/v2";
import { Button, CallOut } from "coral-ui/components/v3";
import { MediaSettingsContainer_viewer } from "coral-stream/__generated__/MediaSettingsContainer_viewer.graphql";
import UpdateUserMediaSettingsMutation from "./UpdateUserMediaSettingsMutation";
import styles from "./MediaSettingsContainer.css";
interface Props {
viewer: MediaSettingsContainer_viewer;
}
const MediaSettingsContainer: FunctionComponent<Props> = ({ viewer }) => {
const updateMediaSettings = useMutation(UpdateUserMediaSettingsMutation);
const [showSuccess, setShowSuccess] = useState(false);
const [showError, setShowError] = useState(false);
const closeSuccess = useCallback(() => {
setShowSuccess(false);
}, [setShowSuccess]);
const closeError = useCallback(() => {
setShowError(false);
}, [setShowError]);
const onSubmit = useCallback(
async (values) => {
try {
await updateMediaSettings(values);
setShowSuccess(true);
} catch (err) {
if (err instanceof InvalidRequestError) {
return err.invalidArgs;
}
setShowError(true);
return {
[FORM_ERROR]: err.message,
};
}
return;
},
[updateMediaSettings, setShowSuccess, setShowError]
);
return (
<HorizontalGutter>
<Form initialValues={viewer.mediaSettings} onSubmit={onSubmit}>
{({
handleSubmit,
submitting,
submitError,
pristine,
submitSucceeded,
}) => (
<form className={styles.form} onSubmit={handleSubmit}>
<Localized id="profile-preferences-mediaPreferences">
<div className={styles.title}>Media Preferences</div>
</Localized>
<div className={styles.options}>
<FieldSet>
<FormField>
<Field name="unfurlEmbeds" type="checkbox">
{({ input }) => (
<CheckBox {...input} id={input.name} variant="streamBlue">
<Localized id="profile-preferences-mediaPreferences-alwaysShow">
<div>Always show GIFs, Tweets, YouTube, etc.</div>
</Localized>
</CheckBox>
)}
</Field>
</FormField>
</FieldSet>
<Localized id="profile-preferences-mediaPreferences-thisMayMake">
<div className={styles.checkBoxDescription}>
This may make the comments slower to load
</div>
</Localized>
</div>
<div
className={cn(styles.updateButton, {
[styles.updateButtonNotification]: showSuccess || showError,
})}
>
<Localized id="profile-preferences-mediaPreferences-update">
<Button
type="submit"
disabled={submitting || pristine}
className={CLASSES.mediaPreferences.updateButton}
upperCase
>
Update
</Button>
</Localized>
</div>
{((submitError && showError) ||
(submitSucceeded && showSuccess)) && (
<div className={styles.callOut}>
{submitError && showError && (
<CallOut
color="error"
onClose={closeError}
icon={<Icon size="sm">warning</Icon>}
titleWeight="semiBold"
title={<span>{submitError}</span>}
/>
)}
{submitSucceeded && showSuccess && (
<CallOut
color="success"
onClose={closeSuccess}
icon={<Icon size="sm">check_circle</Icon>}
titleWeight="semiBold"
title={
<Localized id="profile-preferences-mediaPreferences-preferencesUpdated">
<span>Your media preferences have been updated</span>
</Localized>
}
/>
)}
</div>
)}
</form>
)}
</Form>
</HorizontalGutter>
);
};
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment MediaSettingsContainer_viewer on User {
id
mediaSettings {
unfurlEmbeds
}
}
`,
})(MediaSettingsContainer);
export default enhanced;
@@ -2,11 +2,12 @@ import React, { FunctionComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "coral-framework/lib/relay";
import { HorizontalGutter } from "coral-ui/components/v2";
import { HorizontalGutter, HorizontalRule } from "coral-ui/components/v2";
import { PreferencesContainer_viewer } from "coral-stream/__generated__/PreferencesContainer_viewer.graphql";
import IgnoreUserSettingsContainer from "./IgnoreUserSettingsContainer";
import MediaSettingsContainer from "./MediaSettingsContainer";
import NotificationSettingsContainer from "./NotificationSettingsContainer";
interface Props {
@@ -17,6 +18,8 @@ const PreferencesContainer: FunctionComponent<Props> = (props) => {
return (
<HorizontalGutter spacing={4}>
<NotificationSettingsContainer viewer={props.viewer} />
<MediaSettingsContainer viewer={props.viewer} />
<HorizontalRule></HorizontalRule>
<IgnoreUserSettingsContainer viewer={props.viewer} />
</HorizontalGutter>
);
@@ -27,6 +30,7 @@ const enhanced = withFragmentContainer<Props>({
fragment PreferencesContainer_viewer on User {
...NotificationSettingsContainer_viewer
...IgnoreUserSettingsContainer_viewer
...MediaSettingsContainer_viewer
}
`,
})(PreferencesContainer);
@@ -0,0 +1,68 @@
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 { UpdateUserMediaSettingsEvent } from "coral-stream/events";
import { UpdateUserMediaSettingsMutation as MutationTypes } from "coral-stream/__generated__/UpdateUserMediaSettingsMutation.graphql";
let clientMutationId = 0;
const UpdateUserMediaSettingsMutation = createMutation(
"updateUserMediaSettings",
async (
environment: Environment,
input: MutationInput<MutationTypes>,
{ eventEmitter }: CoralContext
) => {
const updateMediaSettings = UpdateUserMediaSettingsEvent.begin(
eventEmitter,
{
unfurlEmbeds: input.unfurlEmbeds,
}
);
try {
const result = await commitMutationPromiseNormalized<MutationTypes>(
environment,
{
mutation: graphql`
mutation UpdateUserMediaSettingsMutation(
$input: UpdateUserMediaSettingsInput!
) {
updateUserMediaSettings(input: $input) {
user {
id
mediaSettings {
unfurlEmbeds
}
}
clientMutationId
}
}
`,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
}
);
updateMediaSettings.success();
return result;
} catch (error) {
updateMediaSettings.error({
message: error.message,
code: error.code,
});
throw error;
}
}
);
export default UpdateUserMediaSettingsMutation;
+5
View File
@@ -25,6 +25,7 @@ import {
updateAvatar,
updateEmail,
updateEmailByID,
updateMediaSettings,
updateModerationScopes,
updateNotificationSettings,
updatePassword,
@@ -62,6 +63,7 @@ import {
GQLUpdatePasswordInput,
GQLUpdateUserAvatarInput,
GQLUpdateUserEmailInput,
GQLUpdateUserMediaSettingsInput,
GQLUpdateUserModerationScopesInput,
GQLUpdateUsernameInput,
GQLUpdateUserRoleInput,
@@ -206,6 +208,9 @@ export const Users = (ctx: GraphContext) => ({
updateNotificationSettings: async (
input: WithoutMutationID<GQLUpdateNotificationSettingsInput>
) => updateNotificationSettings(ctx.mongo, ctx.tenant, ctx.user!, input),
updateUserMediaSettings: async (
input: WithoutMutationID<GQLUpdateUserMediaSettingsInput>
) => updateMediaSettings(ctx.mongo, ctx.tenant, ctx.user!, input),
updateUserAvatar: async (input: GQLUpdateUserAvatarInput) =>
updateAvatar(ctx.mongo, ctx.tenant, input.userID, input.avatar),
updateUserRole: async (input: GQLUpdateUserRoleInput) =>
@@ -31,6 +31,14 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
user: await ctx.mutators.Users.updateNotificationSettings(input),
clientMutationId,
}),
updateUserMediaSettings: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
user: await ctx.mutators.Users.updateUserMediaSettings(input),
clientMutationId,
}),
updateSettings: async (
source,
{ input: { clientMutationId, ...input } },
@@ -2184,6 +2184,17 @@ enum DIGEST_FREQUENCY {
HOURLY
}
"""
UserMediaSettings are the user's preferences around embed stream behaviour.
"""
type UserMediaSettings {
"""
unfurlEmbeds is whether the user has chosen to immediately show embed contents
without having to click "Show Tweet", "Show GIF", etc.
"""
unfurlEmbeds: Boolean
}
"""
UserNotificationSettings stores the notification settings for a given User.
"""
@@ -2458,6 +2469,11 @@ type User {
"""
ssoURL: String
@auth(userIDField: "id", permit: [SUSPENDED, BANNED, PENDING_DELETION])
"""
mediaSettings are the user's preferences around media stream behaviour.
"""
mediaSettings: UserMediaSettings
}
"""
@@ -3625,6 +3641,16 @@ type UpdateNotificationSettingsPayload {
clientMutationId: String!
}
input UpdateUserMediaSettingsInput {
unfurlEmbeds: Boolean
clientMutationId: String!
}
type UpdateUserMediaSettingsPayload {
user: User!
clientMutationId: String!
}
##################
## createComment
##################
@@ -7335,6 +7361,14 @@ type Mutation {
): UpdateNotificationSettingsPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
"""
updateUserMediaSettings can be used to update the media preferences for the
current logged in user.
"""
updateUserMediaSettings(
input: UpdateUserMediaSettingsInput!
): UpdateUserMediaSettingsPayload! @auth
"""
updateUserEmail allows administrators to update a given User's email address
to the one provided.
+40
View File
@@ -37,6 +37,7 @@ import {
GQLPremodStatus,
GQLSuspensionStatus,
GQLTimeRange,
GQLUpdateUserMediaSettingsInput,
GQLUSER_ROLE,
GQLUsernameStatus,
GQLUserNotificationSettings,
@@ -2319,6 +2320,45 @@ export async function updateUserNotificationSettings(
return result.value;
}
export type UpdateUserMediaSettingsInput = Partial<
GQLUpdateUserMediaSettingsInput
>;
export async function updateUserMediaSettings(
mongo: Db,
tenantID: string,
id: string,
settings: UpdateUserMediaSettingsInput
) {
const result = await collection(mongo).findOneAndUpdate(
{
id,
tenantID,
},
{
$set: dotize({
mediaSettings: settings,
}),
},
{
// False to return the updated document instead of the original
// document.
returnOriginal: false,
}
);
if (!result.value) {
// Get the user so we can figure out why the update operation failed.
const user = await retrieveUser(mongo, tenantID, id);
if (!user) {
throw new UserNotFoundError(id);
}
throw new Error("an unexpected error occurred");
}
return result.value;
}
/**
* insertUserNotificationDigests will push the notification contexts onto the
* User so that notifications can now be queued.
+11
View File
@@ -68,6 +68,8 @@ import {
suspendUser,
updateUserAvatar,
updateUserEmail,
updateUserMediaSettings,
UpdateUserMediaSettingsInput,
updateUserModerationScopes,
updateUserNotificationSettings,
updateUserPassword,
@@ -1275,6 +1277,15 @@ export async function updateNotificationSettings(
return updateUserNotificationSettings(mongo, tenant.id, user.id, settings);
}
export async function updateMediaSettings(
mongo: Db,
tenant: Tenant,
user: User,
settings: UpdateUserMediaSettingsInput
) {
return updateUserMediaSettings(mongo, tenant.id, user.id, settings);
}
function userLastCommentIDKey(
tenant: Pick<Tenant, "id">,
user: Pick<User, "id">
+9
View File
@@ -338,6 +338,15 @@ profile-commentHistory-loadMore = Load More
profile-commentHistory-empty = You have not written any comments
profile-commentHistory-empty-subheading = A history of your comments will appear here
### Preferences
profile-preferences-mediaPreferences = Media Preferences
profile-preferences-mediaPreferences-alwaysShow = Always show GIFs, Tweets, YouTube, etc.
profile-preferences-mediaPreferences-thisMayMake = This may make the comments slower to load
profile-preferences-mediaPreferences-update = Update
profile-preferences-mediaPreferences-preferencesUpdated =
Your media preferences have been updated
### Account
profile-account-ignoredCommenters = Ignored Commenters
profile-account-ignoredCommenters-description =