From 710887818e65b40e512989a5f39d1589077460b4 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Tue, 20 Apr 2021 13:44:15 +0800 Subject: [PATCH 1/9] Fix lint --- ui/component/tagsSearch/view.jsx | 52 +++++++++++++++----------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/ui/component/tagsSearch/view.jsx b/ui/component/tagsSearch/view.jsx index 58ef31378e9..2f3a024788d 100644 --- a/ui/component/tagsSearch/view.jsx +++ b/ui/component/tagsSearch/view.jsx @@ -11,12 +11,12 @@ type Props = { tagsPassedIn: Array, unfollowedTags: Array, followedTags: Array, - doToggleTagFollowDesktop: string => void, - doAddTag: string => void, - onSelect?: Tag => void, + doToggleTagFollowDesktop: (string) => void, + doAddTag: (string) => void, + onSelect?: (Tag) => void, suggestMature?: boolean, disableAutoFocus?: boolean, - onRemove: Tag => void, + onRemove: (Tag) => void, placeholder?: string, label?: string, disabled?: boolean, @@ -54,7 +54,7 @@ export default function TagsSearch(props: Props) { user, } = 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; }; @@ -62,9 +62,9 @@ export default function TagsSearch(props: Props) { // 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); @@ -72,7 +72,7 @@ export default function TagsSearch(props: Props) { 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--; } @@ -80,12 +80,10 @@ export default function TagsSearch(props: Props) { // 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'); } @@ -108,19 +106,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); } @@ -136,13 +134,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) { @@ -171,8 +169,8 @@ export default function TagsSearch(props: Props) { {countWithoutSpecialTags === 0 && } {Boolean(tagsPassedIn.length) && tagsPassedIn - .filter(t => !UTILITY_TAGS.includes(t.name)) - .map(tag => ( + .filter((t) => !UTILITY_TAGS.includes(t.name)) + .map((tag) => ( handleSubmit(e) : e => handleTagClick(newTag)} + onClick={newTag.includes('') ? (e) => handleSubmit(e) : (e) => handleTagClick(newTag)} /> )} - {suggestedTags.map(tag => ( + {suggestedTags.map((tag) => ( {experimentalFeature && - onSelect && ( // onSelect ensures this does not appear on TagFollow + onSelect && ( // onSelect ensures this does not appear on TagFollow - {UTILITY_TAGS.map(t => ( + {UTILITY_TAGS.map((t) => ( 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)} /> ))} From a0448a266deb221ed1c60728afb4c0ad1b6b281b Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 21 Apr 2021 16:38:01 +0800 Subject: [PATCH 2/9] TagsSearch: tweaks to allow re-use beyond tags - Allow 'followed' and 'unfollowed' list to be overridden via props. - Allow labels to be overridden via props. - Add ability to disable "Suggestions" --- ui/component/tagsSearch/index.js | 6 ++-- ui/component/tagsSearch/view.jsx | 56 ++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/ui/component/tagsSearch/index.js b/ui/component/tagsSearch/index.js index a5eefcc8628..c280b852a51 100644 --- a/ui/component/tagsSearch/index.js +++ b/ui/component/tagsSearch/index.js @@ -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), }); diff --git a/ui/component/tagsSearch/view.jsx b/ui/component/tagsSearch/view.jsx index 2f3a024788d..a903cf567c9 100644 --- a/ui/component/tagsSearch/view.jsx +++ b/ui/component/tagsSearch/view.jsx @@ -14,11 +14,14 @@ type Props = { doToggleTagFollowDesktop: (string) => void, doAddTag: (string) => void, onSelect?: (Tag) => void, + hideSuggestions?: boolean, suggestMature?: boolean, disableAutoFocus?: boolean, onRemove: (Tag) => void, placeholder?: string, label?: string, + labelAddNew?: string, + labelSuggestions?: string, disabled?: boolean, limitSelect?: number, limitShow?: number, @@ -44,10 +47,13 @@ export default function TagsSearch(props: Props) { doAddTag, onSelect, onRemove, + hideSuggestions, suggestMature, disableAutoFocus, placeholder, label, + labelAddNew, + labelSuggestions, disabled, limitSelect = TAG_FOLLOW_MAX, limitShow = 5, @@ -189,31 +195,33 @@ export default function TagsSearch(props: Props) { type="text" value={newTag} disabled={disabled} - label={__('Add Tags')} + label={labelAddNew || __('Add Tags')} /> -
- -
    - {Boolean(newTag.length) && !suggestedTags.includes(newTag) && ( - handleSubmit(e) : (e) => handleTagClick(newTag)} - /> - )} - {suggestedTags.map((tag) => ( - handleTagClick(tag)} - /> - ))} -
-
+ {!hideSuggestions && ( +
+ +
    + {Boolean(newTag.length) && !suggestedTags.includes(newTag) && ( + handleSubmit(e) : (e) => handleTagClick(newTag)} + /> + )} + {suggestedTags.map((tag) => ( + handleTagClick(tag)} + /> + ))} +
+
+ )}
{experimentalFeature && onSelect && ( // onSelect ensures this does not appear on TagFollow From 929fb828c63377a245068b427481e18e379d1a24 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Thu, 22 Apr 2021 16:32:57 +0800 Subject: [PATCH 3/9] fetchCommentsApi: Relay error messages to the client's 'catch' block. --- ui/comments.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/comments.js b/ui/comments.js index eb94edca79a..77c0bfc91da 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -35,7 +35,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; From c26c49a766133d6333f55cedc051ac3c19ad4040 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Tue, 20 Apr 2021 16:40:53 +0800 Subject: [PATCH 4/9] Commentron Settings API --- flow-typed/Comment.js | 38 +++++- ui/comments.js | 5 + ui/constants/action_types.js | 6 + ui/redux/actions/comments.js | 209 +++++++++++++++++++++++++++++++++ ui/redux/reducers/comments.js | 47 ++++++++ ui/redux/selectors/comments.js | 6 + 6 files changed, 310 insertions(+), 1 deletion(-) diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index fae198fd8c5..2de7ab930dd 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -15,6 +15,14 @@ declare type Comment = { support_amount: number, }; +declare type PerChannelSettings = { + words?: Array, + 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 }, @@ -33,6 +41,9 @@ declare type CommentsState = { fetchingModerationBlockList: boolean, blockingByUri: {}, unBlockingByUri: {}, + settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings + fetchingSettings: boolean, + fetchingBlockedWords: boolean, }; declare type CommentReactParams = { @@ -44,7 +55,6 @@ declare type CommentReactParams = { remove?: boolean, }; -// @flow declare type CommentListParams = { page: number, page_size: number, @@ -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 +}; diff --git a/ui/comments.js b/ui/comments.js index 77c0bfc91da..0c7f97d04fa 100644 --- a/ui/comments.js +++ b/ui/comments.js @@ -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), }; diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index e9f15a73eef..f405876bcdd 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -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'; diff --git a/ui/redux/actions/comments.js b/ui/redux/actions/comments.js index 0130ec7534c..b98b01b08ac 100644 --- a/ui/redux/actions/comments.js +++ b/ui/redux/actions/comments.js @@ -682,3 +682,212 @@ export const doUpdateBlockListForPublishedChannel = (channelClaim: ChannelClaim) ); }; }; + +export const doFetchCreatorSettings = (channelClaimIds: Array = []) => { + return async (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const myChannels = selectMyChannelClaims(state); + + dispatch({ + type: ACTIONS.COMMENT_FETCH_SETTINGS_STARTED, + }); + + let channelSignatures = []; + if (myChannels) { + for (const channelClaim of myChannels) { + if (channelClaimIds.length !== 0 && !channelClaimIds.includes(channelClaim.claim_id)) { + continue; + } + + try { + const channelSignature = await Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }); + + channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name }); + } catch (e) {} + } + } + + return Promise.all( + channelSignatures.map((signatureData) => + Comments.setting_list({ + channel_name: signatureData.name, + channel_id: signatureData.claim_id, + signature: signatureData.signature, + signing_ts: signatureData.signing_ts, + }) + ) + ) + .then((settings) => { + const settingsByChannelId = {}; + + for (let i = 0; i < channelSignatures.length; ++i) { + const channelId = channelSignatures[i].claim_id; + settingsByChannelId[channelId] = settings[i]; + + settingsByChannelId[channelId].words = settingsByChannelId[channelId].words.split(','); + + delete settingsByChannelId[channelId].channel_name; + delete settingsByChannelId[channelId].channel_id; + delete settingsByChannelId[channelId].signature; + delete settingsByChannelId[channelId].signing_ts; + } + + dispatch({ + type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED, + data: settingsByChannelId, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.COMMENT_FETCH_SETTINGS_FAILED, + }); + }); + }; +}; + +/** + * Updates creator settings, except for 'Words', which will be handled by + * 'doCommentWords, doCommentBlockWords, etc.' + * + * @param channelClaim + * @param settings + * @returns {function(Dispatch, GetState): Promise|Promise|*} + */ +export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: PerChannelSettings) => { + return async (dispatch: Dispatch, getState: GetState) => { + let channelSignature: ?{ + signature: string, + signing_ts: string, + }; + try { + channelSignature = await Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }); + } catch (e) {} + + if (!channelSignature) { + return; + } + + return Comments.setting_update({ + channel_name: channelClaim.name, + channel_id: channelClaim.claim_id, + signature: channelSignature.signature, + signing_ts: channelSignature.signing_ts, + ...settings, + }).catch((err) => { + dispatch( + doToast({ + message: err.message, + isError: true, + }) + ); + }); + }; +}; + +export const doCommentWords = (channelClaim: ChannelClaim, words: Array, isUnblock: boolean) => { + return async (dispatch: Dispatch, getState: GetState) => { + let channelSignature: ?{ + signature: string, + signing_ts: string, + }; + try { + channelSignature = await Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }); + } catch (e) {} + + if (!channelSignature) { + return; + } + + const cmd = isUnblock ? Comments.setting_unblock_word : Comments.setting_block_word; + + return cmd({ + channel_name: channelClaim.name, + channel_id: channelClaim.claim_id, + words: words.join(','), + signature: channelSignature.signature, + signing_ts: channelSignature.signing_ts, + }).catch((err) => { + dispatch( + doToast({ + message: err.message, + isError: true, + }) + ); + }); + }; +}; + +export const doCommentBlockWords = (channelClaim: ChannelClaim, words: Array) => { + return (dispatch: Dispatch) => { + return dispatch(doCommentWords(channelClaim, words, false)); + }; +}; + +export const doCommentUnblockWords = (channelClaim: ChannelClaim, words: Array) => { + return (dispatch: Dispatch) => { + return dispatch(doCommentWords(channelClaim, words, true)); + }; +}; + +export const doFetchBlockedWords = () => { + return async (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const myChannels = selectMyChannelClaims(state); + + dispatch({ + type: ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_STARTED, + }); + + let channelSignatures = []; + if (myChannels) { + for (const channelClaim of myChannels) { + try { + const channelSignature = await Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }); + + channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name }); + } catch (e) {} + } + } + + return Promise.all( + channelSignatures.map((signatureData) => + Comments.setting_list_blocked_words({ + channel_name: signatureData.name, + channel_id: signatureData.claim_id, + signature: signatureData.signature, + signing_ts: signatureData.signing_ts, + }) + ) + ) + .then((blockedWords) => { + const blockedWordsByChannelId = {}; + + for (let i = 0; i < channelSignatures.length; ++i) { + const claim_id = channelSignatures[i].claim_id; + blockedWordsByChannelId[claim_id] = blockedWords[i].word_list; + } + + dispatch({ + type: ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_COMPLETED, + data: blockedWordsByChannelId, + }); + }) + .catch(() => { + dispatch({ + type: ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_FAILED, + }); + }); + }; +}; diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index c94236072f6..e0c81cad39e 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -24,6 +24,9 @@ const defaultState: CommentsState = { fetchingModerationBlockList: false, blockingByUri: {}, unBlockingByUri: {}, + settingsByChannelId: {}, // ChannelId -> PerChannelSettings + fetchingSettings: false, + fetchingBlockedWords: false, }; export default handleActions( @@ -452,6 +455,50 @@ export default handleActions( moderationBlockList: newModerationBlockList, }; }, + + [ACTIONS.COMMENT_FETCH_SETTINGS_STARTED]: (state: CommentsState, action: any) => ({ + ...state, + fetchingSettings: true, + }), + [ACTIONS.COMMENT_FETCH_SETTINGS_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + fetchingSettings: false, + }), + [ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED]: (state: CommentsState, action: any) => { + return { + ...state, + settingsByChannelId: action.data, + fetchingSettings: false, + }; + }, + + [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_STARTED]: (state: CommentsState, action: any) => ({ + ...state, + fetchingBlockedWords: true, + }), + [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + fetchingBlockedWords: false, + }), + [ACTIONS.COMMENT_FETCH_BLOCKED_WORDS_COMPLETED]: (state: CommentsState, action: any) => { + const blockedWordsByChannelId = action.data; + const settingsByChannelId = Object.assign({}, state.settingsByChannelId); + + // blockedWordsByChannelId: {string: [string]} + Object.entries(blockedWordsByChannelId).forEach((x) => { + const channelId = x[0]; + if (!settingsByChannelId[channelId]) { + settingsByChannelId[channelId] = {}; + } + settingsByChannelId[channelId].words = x[1]; + }); + + return { + ...state, + settingsByChannelId, + fetchingBlockedWords: false, + }; + }, }, defaultState ); diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 1db8981bf72..ee4590262fb 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -124,6 +124,12 @@ export const makeSelectOthersReactionsForComment = (commentId: string) => export const selectPendingCommentReacts = createSelector(selectState, (state) => state.pendingCommentReactions); +export const selectSettingsByChannelId = createSelector(selectState, (state) => state.settingsByChannelId); + +export const selectFetchingCreatorSettings = createSelector(selectState, (state) => state.fetchingSettings); + +export const selectFetchingBlockedWords = createSelector(selectState, (state) => state.fetchingBlockedWords); + export const makeSelectCommentsForUri = (uri: string) => createSelector( selectCommentsByClaimId, From f6ffc4fdb34b310c9fb6812871e5797dfdf102f7 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Wed, 21 Apr 2021 17:25:06 +0800 Subject: [PATCH 5/9] Add/implement SettingsCreatorPage --- static/app-strings.json | 11 ++ ui/component/router/view.jsx | 2 + ui/constants/pages.js | 1 + ui/page/settings/index.js | 3 +- ui/page/settings/view.jsx | 18 +++ ui/page/settingsCreator/index.js | 30 ++++ ui/page/settingsCreator/view.jsx | 226 +++++++++++++++++++++++++++++++ ui/scss/component/_tags.scss | 12 ++ ui/scss/init/_base-theme.scss | 6 + ui/scss/themes/dark.scss | 6 + 10 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 ui/page/settingsCreator/index.js create mode 100644 ui/page/settingsCreator/view.jsx diff --git a/static/app-strings.json b/static/app-strings.json index d5abacd9fae..c1c900ac06c 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1860,6 +1860,17 @@ "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.", "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?", diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 555567d2521..2b43f31fc4d 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -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'; @@ -283,6 +284,7 @@ function AppRouter(props: Props) { + diff --git a/ui/constants/pages.js b/ui/constants/pages.js index c4f0506d741..2e36acda2e7 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -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'; diff --git a/ui/page/settings/index.js b/ui/page/settings/index.js index 5718dfa0456..c30d53f35db 100644 --- a/ui/page/settings/index.js +++ b/ui/page/settings/index.js @@ -16,7 +16,7 @@ import { selectLanguage, selectShowMatureContent, } from 'redux/selectors/settings'; -import { doWalletStatus, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux'; +import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux'; import SettingsPage from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; @@ -37,6 +37,7 @@ const select = (state) => ({ hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state), darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state), language: selectLanguage(state), + myChannelUrls: selectMyChannelUrls(state), }); const perform = (dispatch) => ({ diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index 06bbb5e99a9..cb7e86af7f7 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -71,6 +71,7 @@ type Props = { language?: string, enterSettings: () => void, exitSettings: () => void, + myChannelUrls: ?Array, }; type State = { @@ -187,6 +188,7 @@ class SettingsPage extends React.PureComponent { darkModeTimes, clearCache, openModal, + myChannelUrls, } = this.props; const { storedPassword } = this.state; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; @@ -468,6 +470,22 @@ class SettingsPage extends React.PureComponent { } /> + {myChannelUrls && myChannelUrls.length > 0 && ( + +