Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Muted Words Settings Page #5934

Merged
merged 9 commits into from
May 26, 2021
38 changes: 37 additions & 1 deletion flow-typed/Comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ declare type Comment = {
support_amount: number,
};

declare type PerChannelSettings = {
words?: Array<string>,
comments_enabled?: boolean,
min_tip_amount_comment?: number,
min_tip_amount_super_chat?: number,
slow_mode_min_gap?: number,
};

// todo: relate individual comments to their commentId
declare type CommentsState = {
commentsByUri: { [string]: string },
Expand All @@ -33,6 +41,9 @@ declare type CommentsState = {
fetchingModerationBlockList: boolean,
blockingByUri: {},
unBlockingByUri: {},
settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings
fetchingSettings: boolean,
fetchingBlockedWords: boolean,
};

declare type CommentReactParams = {
Expand All @@ -44,7 +55,6 @@ declare type CommentReactParams = {
remove?: boolean,
};

// @flow
declare type CommentListParams = {
page: number,
page_size: number,
Expand Down Expand Up @@ -76,3 +86,29 @@ declare type CommentCreateParams = {
declare type SuperListParams = {};

declare type ModerationBlockParams = {};

declare type SettingsParams = {
channel_name: string,
channel_id: string,
signature: string,
signing_ts: string,
};

declare type UpdateSettingsParams = {
channel_name: string,
channel_id: string,
signature: string,
signing_ts: string,
comments_enabled?: boolean,
min_tip_amount_comment?: number,
min_tip_amount_super_chat?: number,
slow_mode_min_gap?: number,
}

declare type BlockWordParams = {
channel_name: string,
channel_id: string,
signature: string,
signing_ts: string,
words: string, // CSV list of containing words to block comment on content
};
15 changes: 15 additions & 0 deletions static/app-strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,8 @@
"Controversial": "Controversial",
"Show Replies": "Show Replies",
"Unable to create comment, please try again later.": "Unable to create comment, please try again later.",
"Unable to comment. This channel has blocked you.": "Unable to comment. This channel has blocked you.",
"The comment contains contents that are blocked by %author%": "The comment contains contents that are blocked by %author%",
"Your channel is still being setup, try again in a few moments.": "Your channel is still being setup, try again in a few moments.",
"Unable to delete this comment, please try again later.": "Unable to delete this comment, please try again later.",
"Unable to edit this comment, please try again later.": "Unable to edit this comment, please try again later.",
Expand Down Expand Up @@ -1860,6 +1862,19 @@
"Law URL": "Law URL",
"Clarification": "Clarification",
"Client name": "Client name",
"Creator settings": "Creator settings",
"Muted words": "Muted words",
"Add words": "Add words",
"Suggestions": "Suggestions",
"Add words to block": "Add words to block",
"Enable comments for channel.": "Enable comments for channel.",
"Minimum time gap in seconds for Slow Mode in livestream chat.": "Minimum time gap in seconds for Slow Mode in livestream chat.",
"Minimum %lbc% tip amount for comments": "Minimum %lbc% tip amount for comments",
"Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.": "Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.",
"Minimum %lbc% tip amount for hyperchats": "Minimum %lbc% tip amount for hyperchats",
"Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.": "Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.",
"Settings unavailable for this channel": "Settings unavailable for this channel",
"This channel isn't staking enough LBRY Credits to enable Creator Settings.": "This channel isn't staking enough LBRY Credits to enable Creator Settings.",
"We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.",
"Help LBRY Save Crypto": "Help LBRY Save Crypto",
"The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?",
Expand Down
12 changes: 11 additions & 1 deletion ui/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const Comments = {
comment_list: (params: CommentListParams) => fetchCommentsApi('comment.List', params),
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params),
setting_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params),
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
setting_list_blocked_words: (params: SettingsParams) => fetchCommentsApi('setting.ListBlockedWords', params),
setting_update: (params: UpdateSettingsParams) => fetchCommentsApi('setting.Update', params),
super_list: (params: SuperListParams) => fetchCommentsApi('comment.SuperChatList', params),
};

Expand All @@ -35,7 +40,12 @@ function fetchCommentsApi(method: string, params: {}) {

return fetch(url, options)
.then((res) => res.json())
.then((res) => res.result);
.then((res) => {
if (res.error) {
throw new Error(res.error.message);
}
return res.result;
});
}

export default Comments;
1 change: 1 addition & 0 deletions ui/component/channelEdit/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ function ChannelForm(props: Props) {
<TagsSearch
suggestMature={!SIMPLE_SITE}
disableAutoFocus
disableControlTags
limitSelect={MAX_TAG_SELECT}
tagsPassedIn={params.tags || []}
label={__('Selected Tags')}
Expand Down
2 changes: 2 additions & 0 deletions ui/component/router/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
import SettingsPage from 'page/settings';
import SettingsNotificationsPage from 'page/settingsNotifications';
import SettingsAdvancedPage from 'page/settingsAdvanced';
import SettingsCreatorPage from 'page/settingsCreator';
import HelpPage from 'page/help';
// @if TARGET='app'
import BackupPage from 'page/backup';
Expand Down Expand Up @@ -283,6 +284,7 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING_MANAGE}`} component={TagsFollowingManagePage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_BLOCKED_MUTED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} />
Expand Down
6 changes: 3 additions & 3 deletions ui/component/tagsSearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { doToggleTagFollowDesktop, doAddTag, doDeleteTag } from 'redux/actions/t
import { selectUser } from 'redux/selectors/user';
import DiscoveryFirstRun from './view';

const select = state => ({
unfollowedTags: selectUnfollowedTags(state),
followedTags: selectFollowedTags(state),
const select = (state, props) => ({
unfollowedTags: props.unfollowedTags || selectUnfollowedTags(state),
followedTags: props.followedTags || selectFollowedTags(state),
user: selectUser(state),
});

Expand Down
107 changes: 58 additions & 49 deletions ui/component/tagsSearch/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ type Props = {
tagsPassedIn: Array<Tag>,
unfollowedTags: Array<Tag>,
followedTags: Array<Tag>,
doToggleTagFollowDesktop: string => void,
doAddTag: string => void,
onSelect?: Tag => void,
doToggleTagFollowDesktop: (string) => void,
doAddTag: (string) => void,
onSelect?: (Tag) => void,
hideSuggestions?: boolean,
suggestMature?: boolean,
disableAutoFocus?: boolean,
onRemove: Tag => void,
onRemove: (Tag) => void,
placeholder?: string,
label?: string,
labelAddNew?: string,
labelSuggestions?: string,
disabled?: boolean,
limitSelect?: number,
limitShow?: number,
user: User,
disableControlTags?: boolean,
};

const UNALLOWED_TAGS = ['lbry-first'];
Expand All @@ -44,48 +48,50 @@ export default function TagsSearch(props: Props) {
doAddTag,
onSelect,
onRemove,
hideSuggestions,
suggestMature,
disableAutoFocus,
placeholder,
label,
labelAddNew,
labelSuggestions,
disabled,
limitSelect = TAG_FOLLOW_MAX,
limitShow = 5,
user,
disableControlTags,
} = props;
const [newTag, setNewTag] = useState('');
const doesTagMatch = name => {
const doesTagMatch = (name) => {
const nextTag = newTag.substr(newTag.lastIndexOf(',') + 1, newTag.length).trim();
return newTag ? name.toLowerCase().includes(nextTag.toLowerCase()) : true;
};

// Make sure there are no duplicates, then trim
// suggestedTags = (followedTags - tagsPassedIn) + unfollowedTags
const experimentalFeature = user && user.experimental_ui;
const followedTagsSet = new Set(followedTags.map(tag => tag.name));
const selectedTagsSet = new Set(tagsPassedIn.map(tag => tag.name));
const unfollowedTagsSet = new Set(unfollowedTags.map(tag => tag.name));
const followedTagsSet = new Set(followedTags.map((tag) => tag.name));
const selectedTagsSet = new Set(tagsPassedIn.map((tag) => tag.name));
const unfollowedTagsSet = new Set(unfollowedTags.map((tag) => tag.name));
const remainingFollowedTagsSet = setDifference(followedTagsSet, selectedTagsSet);
const remainingUnfollowedTagsSet = setDifference(unfollowedTagsSet, selectedTagsSet);
const suggestedTagsSet = setUnion(remainingFollowedTagsSet, remainingUnfollowedTagsSet);

const SPECIAL_TAGS = [...UTILITY_TAGS, 'lbry-first', 'mature'];
let countWithoutSpecialTags = selectedTagsSet.size;

SPECIAL_TAGS.forEach(t => {
SPECIAL_TAGS.forEach((t) => {
if (selectedTagsSet.has(t)) {
countWithoutSpecialTags--;
}
});

// const countWithoutLbryFirst = selectedTagsSet.has('lbry-first') ? selectedTagsSet.size - 1 : selectedTagsSet.size;
const maxed = Boolean(limitSelect && countWithoutSpecialTags >= limitSelect);
const suggestedTags = Array.from(suggestedTagsSet)
.filter(doesTagMatch)
.slice(0, limitShow);
const suggestedTags = Array.from(suggestedTagsSet).filter(doesTagMatch).slice(0, limitShow);

// tack 'mature' onto the end if it's not already in the list
if (!newTag && suggestMature && !suggestedTags.some(tag => tag === 'mature')) {
if (!newTag && suggestMature && !suggestedTags.some((tag) => tag === 'mature')) {
suggestedTags.push('mature');
}

Expand All @@ -108,19 +114,19 @@ export default function TagsSearch(props: Props) {
tags
.split(',')
.slice(0, limitSelect - countWithoutSpecialTags)
.map(newTag => newTag.trim().toLowerCase())
.filter(newTag => !UNALLOWED_TAGS.includes(newTag))
.map((newTag) => newTag.trim().toLowerCase())
.filter((newTag) => !UNALLOWED_TAGS.includes(newTag))
)
);

// Split into individual tags, normalize the tags, and remove duplicates with a set.
if (onSelect) {
const arrOfObjectTags = newTagsArr.map(tag => {
const arrOfObjectTags = newTagsArr.map((tag) => {
return { name: tag };
});
onSelect(arrOfObjectTags);
} else {
newTagsArr.forEach(tag => {
newTagsArr.forEach((tag) => {
if (!unfollowedTags.some(({ name }) => name === tag)) {
doAddTag(tag);
}
Expand All @@ -136,13 +142,13 @@ export default function TagsSearch(props: Props) {
if (onSelect) {
onSelect([{ name: tag }]);
} else {
const wasFollowing = followedTags.map(t => t.name).includes(tag);
const wasFollowing = followedTags.map((t) => t.name).includes(tag);
doToggleTagFollowDesktop(tag);
analytics.tagFollowEvent(tag, !wasFollowing);
}
}
function handleUtilityTagCheckbox(tag: string) {
const selectedTag = tagsPassedIn.find(te => te.name === tag);
const selectedTag = tagsPassedIn.find((te) => te.name === tag);
if (selectedTag) {
onRemove(selectedTag);
} else if (onSelect) {
Expand Down Expand Up @@ -171,8 +177,8 @@ export default function TagsSearch(props: Props) {
{countWithoutSpecialTags === 0 && <Tag key={`placeholder-tag`} name={'example'} disabled type={'remove'} />}
{Boolean(tagsPassedIn.length) &&
tagsPassedIn
.filter(t => !UTILITY_TAGS.includes(t.name))
.map(tag => (
.filter((t) => !UTILITY_TAGS.includes(t.name))
.map((tag) => (
<Tag
key={`passed${tag.name}`}
name={tag.name}
Expand All @@ -191,37 +197,40 @@ export default function TagsSearch(props: Props) {
type="text"
value={newTag}
disabled={disabled}
label={__('Add Tags')}
label={labelAddNew || __('Add Tags')}
/>
<section>
<label>{newTag.length ? __('Matching') : __('Known Tags')}</label>
<ul className="tags">
{Boolean(newTag.length) && !suggestedTags.includes(newTag) && (
<Tag
disabled={newTag !== 'mature' && maxed}
key={`entered${newTag}`}
name={newTag}
type="add"
onClick={newTag.includes('') ? e => handleSubmit(e) : e => handleTagClick(newTag)}
/>
)}
{suggestedTags.map(tag => (
<Tag
disabled={tag !== 'mature' && maxed}
key={`suggested${tag}`}
name={tag}
type="add"
onClick={() => handleTagClick(tag)}
/>
))}
</ul>
</section>
{!hideSuggestions && (
<section>
<label>{labelSuggestions || (newTag.length ? __('Matching') : __('Known Tags'))}</label>
<ul className="tags">
{Boolean(newTag.length) && !suggestedTags.includes(newTag) && (
<Tag
disabled={newTag !== 'mature' && maxed}
key={`entered${newTag}`}
name={newTag}
type="add"
onClick={newTag.includes('') ? (e) => handleSubmit(e) : (e) => handleTagClick(newTag)}
/>
)}
{suggestedTags.map((tag) => (
<Tag
disabled={tag !== 'mature' && maxed}
key={`suggested${tag}`}
name={tag}
type="add"
onClick={() => handleTagClick(tag)}
/>
))}
</ul>
</section>
)}
</fieldset-section>
{experimentalFeature &&
onSelect && ( // onSelect ensures this does not appear on TagFollow
!disableControlTags &&
onSelect && ( // onSelect ensures this does not appear on TagFollow
<fieldset-section>
<label>{__('Control Tags')}</label>
{UTILITY_TAGS.map(t => (
{UTILITY_TAGS.map((t) => (
<FormField
key={t}
name={t}
Expand All @@ -230,10 +239,10 @@ export default function TagsSearch(props: Props) {
label={__(
t
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
)}
checked={tagsPassedIn.some(te => te.name === t)}
checked={tagsPassedIn.some((te) => te.name === t)}
onChange={() => handleUtilityTagCheckbox(t)}
/>
))}
Expand Down
6 changes: 6 additions & 0 deletions ui/constants/action_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ export const COMMENT_MODERATION_BLOCK_FAILED = 'COMMENT_MODERATION_BLOCK_FAILED'
export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED';
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
export const COMMENT_FETCH_SETTINGS_STARTED = 'COMMENT_FETCH_SETTINGS_STARTED';
export const COMMENT_FETCH_SETTINGS_FAILED = 'COMMENT_FETCH_SETTINGS_FAILED';
export const COMMENT_FETCH_SETTINGS_COMPLETED = 'COMMENT_FETCH_SETTINGS_COMPLETED';
export const COMMENT_FETCH_BLOCKED_WORDS_STARTED = 'COMMENT_FETCH_BLOCKED_WORDS_STARTED';
export const COMMENT_FETCH_BLOCKED_WORDS_FAILED = 'COMMENT_FETCH_BLOCKED_WORDS_FAILED';
export const COMMENT_FETCH_BLOCKED_WORDS_COMPLETED = 'COMMENT_FETCH_BLOCKED_WORDS_COMPLETED';
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
export const COMMENT_SUPER_CHAT_LIST_STARTED = 'COMMENT_SUPER_CHAT_LIST_STARTED';
export const COMMENT_SUPER_CHAT_LIST_COMPLETED = 'COMMENT_SUPER_CHAT_LIST_COMPLETED';
Expand Down
1 change: 1 addition & 0 deletions ui/constants/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ exports.SETTINGS = 'settings';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
exports.SETTINGS_CREATOR = 'settings/creator';
exports.SHOW = 'show';
exports.ACCOUNT = 'account';
exports.SEARCH = 'search';
Expand Down
Loading