diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index fae198fd8..2de7ab930 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 77c0bfc91..0c7f97d04 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 e9f15a73e..f405876bc 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 0130ec753..b98b01b08 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 c94236072..e0c81cad3 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 1db8981bf..ee4590262 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,