diff --git a/dist/bundle.es.js b/dist/bundle.es.js index a136b70..dee7cab 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -154,6 +154,23 @@ const PURCHASE_LIST_STARTED = 'PURCHASE_LIST_STARTED'; const PURCHASE_LIST_COMPLETED = 'PURCHASE_LIST_COMPLETED'; const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; +// Comments +const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; +const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; +const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED'; +const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED'; +const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED'; +const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; +const COMMENT_ABANDON_STARTED = 'COMMENT_ABANDON_STARTED'; +const COMMENT_ABANDON_COMPLETED = 'COMMENT_ABANDON_COMPLETED'; +const COMMENT_ABANDON_FAILED = 'COMMENT_ABANDON_FAILED'; +const COMMENT_UPDATE_STARTED = 'COMMENT_UPDATE_STARTED'; +const COMMENT_UPDATE_COMPLETED = 'COMMENT_UPDATE_COMPLETED'; +const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED'; +const COMMENT_HIDE_STARTED = 'COMMENT_HIDE_STARTED'; +const COMMENT_HIDE_COMPLETED = 'COMMENT_HIDE_COMPLETED'; +const COMMENT_HIDE_FAILED = 'COMMENT_HIDE_FAILED'; + // Files const FILE_LIST_STARTED = 'FILE_LIST_STARTED'; const FILE_LIST_SUCCEEDED = 'FILE_LIST_SUCCEEDED'; @@ -289,6 +306,9 @@ const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW'; const TAG_ADD = 'TAG_ADD'; const TAG_DELETE = 'TAG_DELETE'; +// Blocked Channels +const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; + // Sync const USER_STATE_POPULATE = 'USER_STATE_POPULATE'; @@ -417,6 +437,21 @@ var action_types = /*#__PURE__*/Object.freeze({ PURCHASE_LIST_STARTED: PURCHASE_LIST_STARTED, PURCHASE_LIST_COMPLETED: PURCHASE_LIST_COMPLETED, PURCHASE_LIST_FAILED: PURCHASE_LIST_FAILED, + COMMENT_LIST_STARTED: COMMENT_LIST_STARTED, + COMMENT_LIST_COMPLETED: COMMENT_LIST_COMPLETED, + COMMENT_LIST_FAILED: COMMENT_LIST_FAILED, + COMMENT_CREATE_STARTED: COMMENT_CREATE_STARTED, + COMMENT_CREATE_COMPLETED: COMMENT_CREATE_COMPLETED, + COMMENT_CREATE_FAILED: COMMENT_CREATE_FAILED, + COMMENT_ABANDON_STARTED: COMMENT_ABANDON_STARTED, + COMMENT_ABANDON_COMPLETED: COMMENT_ABANDON_COMPLETED, + COMMENT_ABANDON_FAILED: COMMENT_ABANDON_FAILED, + COMMENT_UPDATE_STARTED: COMMENT_UPDATE_STARTED, + COMMENT_UPDATE_COMPLETED: COMMENT_UPDATE_COMPLETED, + COMMENT_UPDATE_FAILED: COMMENT_UPDATE_FAILED, + COMMENT_HIDE_STARTED: COMMENT_HIDE_STARTED, + COMMENT_HIDE_COMPLETED: COMMENT_HIDE_COMPLETED, + COMMENT_HIDE_FAILED: COMMENT_HIDE_FAILED, FILE_LIST_STARTED: FILE_LIST_STARTED, FILE_LIST_SUCCEEDED: FILE_LIST_SUCCEEDED, FETCH_FILE_INFO_STARTED: FETCH_FILE_INFO_STARTED, @@ -529,6 +564,7 @@ var action_types = /*#__PURE__*/Object.freeze({ TOGGLE_TAG_FOLLOW: TOGGLE_TAG_FOLLOW, TAG_ADD: TAG_ADD, TAG_DELETE: TAG_DELETE, + TOGGLE_BLOCK_CHANNEL: TOGGLE_BLOCK_CHANNEL, USER_STATE_POPULATE: USER_STATE_POPULATE }); @@ -2840,7 +2876,7 @@ function doSendDraftTransaction(address, amount) { type: SEND_TRANSACTION_COMPLETED }); dispatch(doToast({ - message: __('You sent ${amount} LBC'), + message: __(`You sent ${amount} LBC`), linkText: __('History'), linkTarget: '/wallet' })); @@ -4874,6 +4910,207 @@ const doDeleteTag = name => ({ } }); +// + +function doCommentList(uri, page = 1, pageSize = 99999) { + return (dispatch, getState) => { + const state = getState(); + const claim = selectClaimsByUri(state)[uri]; + const claimId = claim ? claim.claim_id : null; + + dispatch({ + type: COMMENT_LIST_STARTED + }); + lbryProxy.comment_list({ + claim_id: claimId, + page, + page_size: pageSize + }).then(result => { + const { items: comments } = result; + dispatch({ + type: COMMENT_LIST_COMPLETED, + data: { + comments, + claimId: claimId, + uri: uri + } + }); + }).catch(error => { + console.log(error); + dispatch({ + type: COMMENT_LIST_FAILED, + data: error + }); + }); + }; +} + +function doCommentCreate(comment = '', claim_id = '', channel, parent_id) { + return (dispatch, getState) => { + const state = getState(); + dispatch({ + type: COMMENT_CREATE_STARTED + }); + + const myChannels = selectMyChannelClaims(state); + const namedChannelClaim = myChannels && myChannels.find(myChannel => myChannel.name === channel); + const channel_id = namedChannelClaim.claim_id; + + if (channel_id == null) { + dispatch({ + type: COMMENT_CREATE_FAILED, + data: {} + }); + dispatch(doToast({ + message: 'Channel cannot be anonymous, please select a channel and try again.', + isError: true + })); + return; + } + + return lbryProxy.comment_create({ + comment: comment, + claim_id: claim_id, + channel_id: channel_id, + parent_id: parent_id + }).then(result => { + dispatch({ + type: COMMENT_CREATE_COMPLETED, + data: { + comment: result, + claimId: claim_id + } + }); + }).catch(error => { + dispatch({ + type: COMMENT_CREATE_FAILED, + data: error + }); + dispatch(doToast({ + message: 'Unable to create comment, please try again later.', + isError: true + })); + }); + }; +} + +function doCommentHide(comment_id) { + return dispatch => { + dispatch({ + type: COMMENT_HIDE_STARTED + }); + return lbryProxy.comment_hide({ + comment_ids: [comment_id] + }).then(result => { + dispatch({ + type: COMMENT_HIDE_COMPLETED, + data: result + }); + }).catch(error => { + dispatch({ + type: COMMENT_HIDE_FAILED, + data: error + }); + dispatch(doToast({ + message: 'Unable to hide this comment, please try again later.', + isError: true + })); + }); + }; +} + +function doCommentAbandon(comment_id) { + return dispatch => { + dispatch({ + type: COMMENT_ABANDON_STARTED + }); + return lbryProxy.comment_abandon({ + comment_id: comment_id + }).then(result => { + // Comment may not be deleted if the signing channel can't be signed. + // This will happen if the channel was recently created or abandoned. + if (result.abandoned) { + dispatch({ + type: COMMENT_ABANDON_COMPLETED, + data: { + comment_id: comment_id + } + }); + } else { + dispatch({ + type: COMMENT_ABANDON_FAILED + }); + dispatch(doToast({ + message: 'Your channel is still being setup, try again in a few moments.', + isError: true + })); + } + }).catch(error => { + dispatch({ + type: COMMENT_ABANDON_FAILED, + data: error + }); + dispatch(doToast({ + message: 'Unable to delete this comment, please try again later.', + isError: true + })); + }); + }; +} + +function doCommentUpdate(comment_id, comment) { + // if they provided an empty string, they must have wanted to abandon + if (comment === '') { + return doCommentAbandon(comment_id); + } else { + return dispatch => { + dispatch({ + type: COMMENT_UPDATE_STARTED + }); + return lbryProxy.comment_update({ + comment_id: comment_id, + comment: comment + }).then(result => { + if (result != null) { + dispatch({ + type: COMMENT_UPDATE_COMPLETED, + data: { + comment: result + } + }); + } else { + // the result will return null + dispatch({ + type: COMMENT_UPDATE_FAILED + }); + dispatch(doToast({ + message: 'Your channel is still being setup, try again in a few moments.', + isError: true + })); + } + }).catch(error => { + dispatch({ + type: COMMENT_UPDATE_FAILED, + data: error + }); + dispatch(doToast({ + message: 'Unable to edit this comment, please try again later.', + isError: true + })); + }); + }; + } +} + +// + +const doToggleBlockChannel = uri => ({ + type: TOGGLE_BLOCK_CHANNEL, + data: { + uri + } +}); + var _extends$a = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; /* @@ -5532,32 +5769,185 @@ function claimsReducer(state = defaultState, action) { return state; } +// util for creating reducers +// based off of redux-actions +// https://redux-actions.js.org/docs/api/handleAction.html#handleactions + +// eslint-disable-next-line import/prefer-default-export +const handleActions = (actionMap, defaultState) => (state = defaultState, action) => { + const handler = actionMap[action.type]; + + if (handler) { + const newState = handler(state, action); + return Object.assign({}, state, newState); + } + + // just return the original state if no handler + // returning a copy here breaks redux-persist + return state; +}; + var _extends$c = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const reducers$1 = {}; const defaultState$1 = { + commentById: {}, // commentId -> Comment + byId: {}, // ClaimID -> list of comments + commentsByUri: {}, // URI -> claimId + isLoading: false, + myComments: undefined +}; + +const commentReducer = handleActions({ + [COMMENT_CREATE_STARTED]: (state, action) => _extends$c({}, state, { + isLoading: true + }), + + [COMMENT_CREATE_FAILED]: (state, action) => _extends$c({}, state, { + isLoading: false + }), + + [COMMENT_CREATE_COMPLETED]: (state, action) => { + const { comment, claimId } = action.data; + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + const comments = byId[claimId]; + const newCommentIds = comments.slice(); + + // add the comment by its ID + commentById[comment.comment_id] = comment; + + // push the comment_id to the top of ID list + newCommentIds.unshift(comment.comment_id); + byId[claimId] = newCommentIds; + + return _extends$c({}, state, { + commentById, + byId, + isLoading: false + }); + }, + + [COMMENT_LIST_STARTED]: state => _extends$c({}, state, { isLoading: true }), + + [COMMENT_LIST_COMPLETED]: (state, action) => { + const { comments, claimId, uri } = action.data; + + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + const commentsByUri = Object.assign({}, state.commentsByUri); + + if (comments) { + // we use an Array to preserve order of listing + // in reality this doesn't matter and we can just + // sort comments by their timestamp + const commentIds = Array(comments.length); + + // map the comment_ids to the new comments + for (let i = 0; i < comments.length; i++) { + commentIds[i] = comments[i].comment_id; + commentById[commentIds[i]] = comments[i]; + } + + byId[claimId] = commentIds; + commentsByUri[uri] = claimId; + } + return _extends$c({}, state, { + byId, + commentById, + commentsByUri, + isLoading: false + }); + }, + + [COMMENT_LIST_FAILED]: (state, action) => _extends$c({}, state, { + isLoading: false + }), + [COMMENT_ABANDON_STARTED]: (state, action) => _extends$c({}, state, { + isLoading: true + }), + [COMMENT_ABANDON_COMPLETED]: (state, action) => { + const { comment_id } = action.data; + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + + // to remove the comment and its references + const claimId = commentById[comment_id].claim_id; + for (let i = 0; i < byId[claimId].length; i++) { + if (byId[claimId][i] === comment_id) { + byId[claimId].splice(i, 1); + break; + } + } + delete commentById[comment_id]; + + return _extends$c({}, state, { + commentById, + byId, + isLoading: false + }); + }, + // do nothing + [COMMENT_ABANDON_FAILED]: (state, action) => _extends$c({}, state, { + isLoading: false + }), + // do nothing + [COMMENT_UPDATE_STARTED]: (state, action) => _extends$c({}, state, { + isLoading: true + }), + // replace existing comment with comment returned here under its comment_id + [COMMENT_UPDATE_COMPLETED]: (state, action) => { + const { comment } = action.data; + const commentById = Object.assign({}, state.commentById); + commentById[comment.comment_id] = comment; + + return _extends$c({}, state, { + commentById, + isLoading: false + }); + }, + // nothing can be done here + [COMMENT_UPDATE_FAILED]: (state, action) => _extends$c({}, state, { + isLoading: false + }), + // nothing can really be done here + [COMMENT_HIDE_STARTED]: (state, action) => _extends$c({}, state, { + isLoading: true + }), + [COMMENT_HIDE_COMPLETED]: (state, action) => _extends$c({}, state, { // todo: add HiddenComments state & create selectors + isLoading: false + }), + // nothing can be done here + [COMMENT_HIDE_FAILED]: (state, action) => _extends$c({}, state, { + isLoading: false + }) +}, defaultState$1); + +var _extends$d = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +const reducers$1 = {}; +const defaultState$2 = { positions: {} }; reducers$1[SET_CONTENT_POSITION] = (state, action) => { const { claimId, outpoint, position } = action.data; - return _extends$c({}, state, { - positions: _extends$c({}, state.positions, { - [claimId]: _extends$c({}, state.positions[claimId], { + return _extends$d({}, state, { + positions: _extends$d({}, state.positions, { + [claimId]: _extends$d({}, state.positions[claimId], { [outpoint]: position }) }) }); }; -function contentReducer(state = defaultState$1, action) { +function contentReducer(state = defaultState$2, action) { const handler = reducers$1[action.type]; if (handler) return handler(state, action); return state; } const reducers$2 = {}; -const defaultState$2 = { +const defaultState$3 = { fileListPublishedSort: DATE_NEW, fileListDownloadedSort: DATE_NEW }; @@ -5704,33 +6094,15 @@ reducers$2[SET_FILE_LIST_SORT] = (state, action) => { }); }; -function fileInfoReducer(state = defaultState$2, action) { +function fileInfoReducer(state = defaultState$3, action) { const handler = reducers$2[action.type]; if (handler) return handler(state, action); return state; } -// util for creating reducers -// based off of redux-actions -// https://redux-actions.js.org/docs/api/handleAction.html#handleactions +var _extends$e = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -// eslint-disable-next-line import/prefer-default-export -const handleActions = (actionMap, defaultState) => (state = defaultState, action) => { - const handler = actionMap[action.type]; - - if (handler) { - const newState = handler(state, action); - return Object.assign({}, state, newState); - } - - // just return the original state if no handler - // returning a copy here breaks redux-persist - return state; -}; - -var _extends$d = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -const defaultState$3 = { +const defaultState$4 = { notifications: [], toasts: [], errors: [] @@ -5743,7 +6115,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.push(toast); - return _extends$d({}, state, { + return _extends$e({}, state, { toasts: newToasts }); }, @@ -5751,7 +6123,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.shift(); - return _extends$d({}, state, { + return _extends$e({}, state, { toasts: newToasts }); }, @@ -5762,7 +6134,7 @@ const notificationsReducer = handleActions({ const newNotifications = state.notifications.slice(); newNotifications.push(notification); - return _extends$d({}, state, { + return _extends$e({}, state, { notifications: newNotifications }); }, @@ -5773,7 +6145,7 @@ const notificationsReducer = handleActions({ notifications = notifications.map(pastNotification => pastNotification.id === notification.id ? notification : pastNotification); - return _extends$d({}, state, { + return _extends$e({}, state, { notifications }); }, @@ -5782,7 +6154,7 @@ const notificationsReducer = handleActions({ let newNotifications = state.notifications.slice(); newNotifications = newNotifications.filter(notification => notification.id !== id); - return _extends$d({}, state, { + return _extends$e({}, state, { notifications: newNotifications }); }, @@ -5793,7 +6165,7 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.push(error); - return _extends$d({}, state, { + return _extends$e({}, state, { errors: newErrors }); }, @@ -5801,17 +6173,17 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.shift(); - return _extends$d({}, state, { + return _extends$e({}, state, { errors: newErrors }); } -}, defaultState$3); +}, defaultState$4); -var _extends$e = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$f = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _objectWithoutProperties$4(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } -const defaultState$4 = { +const defaultState$5 = { editingURI: undefined, filePath: undefined, fileDur: 0, @@ -5848,20 +6220,20 @@ const defaultState$4 = { const publishReducer = handleActions({ [UPDATE_PUBLISH_FORM]: (state, action) => { const { data } = action; - return _extends$e({}, state, data); + return _extends$f({}, state, data); }, - [CLEAR_PUBLISH]: state => _extends$e({}, defaultState$4, { + [CLEAR_PUBLISH]: state => _extends$f({}, defaultState$5, { bid: state.bid, optimize: state.optimize }), - [PUBLISH_START]: state => _extends$e({}, state, { + [PUBLISH_START]: state => _extends$f({}, state, { publishing: true, publishSuccess: false }), - [PUBLISH_FAIL]: state => _extends$e({}, state, { + [PUBLISH_FAIL]: state => _extends$f({}, state, { publishing: false }), - [PUBLISH_SUCCESS]: state => _extends$e({}, state, { + [PUBLISH_SUCCESS]: state => _extends$f({}, state, { publishing: false, publishSuccess: true }), @@ -5876,16 +6248,16 @@ const publishReducer = handleActions({ streamName: name }); - return _extends$e({}, defaultState$4, publishData, { + return _extends$f({}, defaultState$5, publishData, { editingURI: uri, uri: shortUri }); } -}, defaultState$4); +}, defaultState$5); -var _extends$f = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$g = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; -const defaultState$5 = { +const defaultState$6 = { isActive: false, // does the user have any typed text in the search input focused: false, // is the search input focused searchQuery: '', // needs to be an empty string for input focusing @@ -5905,23 +6277,23 @@ const defaultState$5 = { }; const searchReducer = handleActions({ - [SEARCH_START]: state => _extends$f({}, state, { + [SEARCH_START]: state => _extends$g({}, state, { searching: true }), [SEARCH_SUCCESS]: (state, action) => { const { query, uris } = action.data; - return _extends$f({}, state, { + return _extends$g({}, state, { searching: false, urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }) }); }, - [SEARCH_FAIL]: state => _extends$f({}, state, { + [SEARCH_FAIL]: state => _extends$g({}, state, { searching: false }), - [RESOLVED_SEARCH_START]: state => _extends$f({}, state, { + [RESOLVED_SEARCH_START]: state => _extends$g({}, state, { searching: true }), [RESOLVED_SEARCH_SUCCESS]: (state, action) => { @@ -5939,24 +6311,24 @@ const searchReducer = handleActions({ // the returned number of urls is less than the page size, so we're on the last page resolvedResultsByQueryLastPageReached[query] = results.length < pageSize; - return _extends$f({}, state, { + return _extends$g({}, state, { searching: false, resolvedResultsByQuery, resolvedResultsByQueryLastPageReached }); }, - [RESOLVED_SEARCH_FAIL]: state => _extends$f({}, state, { + [RESOLVED_SEARCH_FAIL]: state => _extends$g({}, state, { searching: false }), - [UPDATE_SEARCH_QUERY]: (state, action) => _extends$f({}, state, { + [UPDATE_SEARCH_QUERY]: (state, action) => _extends$g({}, state, { searchQuery: action.data.query, isActive: true }), - [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$f({}, state, { - suggestions: _extends$f({}, state.suggestions, { + [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$g({}, state, { + suggestions: _extends$g({}, state.suggestions, { [action.data.query]: action.data.suggestions }) }), @@ -5964,35 +6336,35 @@ const searchReducer = handleActions({ // sets isActive to false so the uri will be populated correctly if the // user is on a file page. The search query will still be present on any // other page - [DISMISS_NOTIFICATION]: state => _extends$f({}, state, { + [DISMISS_NOTIFICATION]: state => _extends$g({}, state, { isActive: false }), - [SEARCH_FOCUS]: state => _extends$f({}, state, { + [SEARCH_FOCUS]: state => _extends$g({}, state, { focused: true }), - [SEARCH_BLUR]: state => _extends$f({}, state, { + [SEARCH_BLUR]: state => _extends$g({}, state, { focused: false }), [UPDATE_SEARCH_OPTIONS]: (state, action) => { const { options: oldOptions } = state; const newOptions = action.data; - const options = _extends$f({}, oldOptions, newOptions); - return _extends$f({}, state, { + const options = _extends$g({}, oldOptions, newOptions); + return _extends$g({}, state, { options }); } -}, defaultState$5); +}, defaultState$6); -var _extends$g = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$h = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function getDefaultKnownTags() { - return DEFAULT_FOLLOWED_TAGS.concat(DEFAULT_KNOWN_TAGS).reduce((tagsMap, tag) => _extends$g({}, tagsMap, { + return DEFAULT_FOLLOWED_TAGS.concat(DEFAULT_KNOWN_TAGS).reduce((tagsMap, tag) => _extends$h({}, tagsMap, { [tag]: { name: tag } }), {}); } -const defaultState$6 = { +const defaultState$7 = { followedTags: [], knownTags: getDefaultKnownTags() }; @@ -6010,7 +6382,7 @@ const tagsReducer = handleActions({ newFollowedTags.push(name); } - return _extends$g({}, state, { + return _extends$h({}, state, { followedTags: newFollowedTags }); }, @@ -6019,10 +6391,10 @@ const tagsReducer = handleActions({ const { knownTags } = state; const { name } = action.data; - let newKnownTags = _extends$g({}, knownTags); + let newKnownTags = _extends$h({}, knownTags); newKnownTags[name] = { name }; - return _extends$g({}, state, { + return _extends$h({}, state, { knownTags: newKnownTags }); }, @@ -6031,11 +6403,11 @@ const tagsReducer = handleActions({ const { knownTags, followedTags } = state; const { name } = action.data; - let newKnownTags = _extends$g({}, knownTags); + let newKnownTags = _extends$h({}, knownTags); delete newKnownTags[name]; const newFollowedTags = followedTags.filter(tag => tag !== name); - return _extends$g({}, state, { + return _extends$h({}, state, { knownTags: newKnownTags, followedTags: newFollowedTags }); @@ -6043,15 +6415,45 @@ const tagsReducer = handleActions({ [USER_STATE_POPULATE]: (state, action) => { const { tags } = action.data; if (Array.isArray(tags)) { - return _extends$g({}, state, { + return _extends$h({}, state, { followedTags: tags }); } - return _extends$g({}, state); + return _extends$h({}, state); } -}, defaultState$6); +}, defaultState$7); -var _extends$h = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$i = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +const defaultState$8 = { + blockedChannels: [] +}; + +const blockedReducer = handleActions({ + [TOGGLE_BLOCK_CHANNEL]: (state, action) => { + const { blockedChannels } = state; + const { uri } = action.data; + let newBlockedChannels = blockedChannels.slice(); + + if (newBlockedChannels.includes(uri)) { + newBlockedChannels = newBlockedChannels.filter(id => id !== uri); + } else { + newBlockedChannels.push(uri); + } + + return { + blockedChannels: newBlockedChannels + }; + }, + [USER_STATE_POPULATE]: (state, action) => { + const { blocked } = action.data; + return _extends$i({}, state, { + blockedChannels: blocked && blocked.length ? blocked : state.blockedChannels + }); + } +}, defaultState$8); + +var _extends$j = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const buildDraftTransaction = () => ({ amount: undefined, @@ -6062,7 +6464,7 @@ const buildDraftTransaction = () => ({ // See details in https://github.com/lbryio/lbry/issues/1307 -const defaultState$7 = { +const defaultState$9 = { balance: undefined, totalBalance: undefined, reservedBalance: undefined, @@ -6103,40 +6505,40 @@ const defaultState$7 = { }; const walletReducer = handleActions({ - [FETCH_TRANSACTIONS_STARTED]: state => _extends$h({}, state, { + [FETCH_TRANSACTIONS_STARTED]: state => _extends$j({}, state, { fetchingTransactions: true }), [FETCH_TRANSACTIONS_COMPLETED]: (state, action) => { - const byId = _extends$h({}, state.transactions); + const byId = _extends$j({}, state.transactions); const { transactions } = action.data; transactions.forEach(transaction => { byId[transaction.txid] = transaction; }); - return _extends$h({}, state, { + return _extends$j({}, state, { transactions: byId, fetchingTransactions: false }); }, [FETCH_TXO_PAGE_STARTED]: state => { - return _extends$h({}, state, { + return _extends$j({}, state, { fetchingTxos: true, fetchingTxosError: undefined }); }, [FETCH_TXO_PAGE_COMPLETED]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { txoPage: action.data, fetchingTxos: false }); }, [FETCH_TXO_PAGE_FAILED]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { txoPage: {}, fetchingTxos: false, fetchingTxosError: action.data @@ -6144,12 +6546,12 @@ const walletReducer = handleActions({ }, [UPDATE_TXO_FETCH_PARAMS]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { txoFetchParams: action.data }); }, - [FETCH_SUPPORTS_STARTED]: state => _extends$h({}, state, { + [FETCH_SUPPORTS_STARTED]: state => _extends$j({}, state, { fetchingSupports: true }), @@ -6162,7 +6564,7 @@ const walletReducer = handleActions({ byOutpoint[`${txid}:${nout}`] = transaction; }); - return _extends$h({}, state, { supports: byOutpoint, fetchingSupports: false }); + return _extends$j({}, state, { supports: byOutpoint, fetchingSupports: false }); }, [ABANDON_SUPPORT_STARTED]: (state, action) => { @@ -6171,7 +6573,7 @@ const walletReducer = handleActions({ currentlyAbandoning[outpoint] = true; - return _extends$h({}, state, { + return _extends$j({}, state, { abandoningSupportsByOutpoint: currentlyAbandoning }); }, @@ -6184,20 +6586,20 @@ const walletReducer = handleActions({ delete currentlyAbandoning[outpoint]; delete byOutpoint[outpoint]; - return _extends$h({}, state, { + return _extends$j({}, state, { supports: byOutpoint, abandoningSupportsById: currentlyAbandoning }); }, [ABANDON_CLAIM_SUPPORT_STARTED]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { abandonClaimSupportError: undefined }); }, [ABANDON_CLAIM_SUPPORT_PREVIEW]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { abandonClaimSupportError: undefined }); }, @@ -6208,36 +6610,36 @@ const walletReducer = handleActions({ pendingtxs[claimId] = { txid, type, effective }; - return _extends$h({}, state, { + return _extends$j({}, state, { pendingSupportTransactions: pendingtxs, abandonClaimSupportError: undefined }); }, [ABANDON_CLAIM_SUPPORT_FAILED]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { abandonClaimSupportError: action.data }); }, [PENDING_SUPPORTS_UPDATED]: (state, action) => { - return _extends$h({}, state, { + return _extends$j({}, state, { pendingSupportTransactions: action.data }); }, - [GET_NEW_ADDRESS_STARTED]: state => _extends$h({}, state, { + [GET_NEW_ADDRESS_STARTED]: state => _extends$j({}, state, { gettingNewAddress: true }), [GET_NEW_ADDRESS_COMPLETED]: (state, action) => { const { address } = action.data; - return _extends$h({}, state, { gettingNewAddress: false, receiveAddress: address }); + return _extends$j({}, state, { gettingNewAddress: false, receiveAddress: address }); }, - [UPDATE_BALANCE]: (state, action) => _extends$h({}, state, { + [UPDATE_BALANCE]: (state, action) => _extends$j({}, state, { totalBalance: action.data.totalBalance, balance: action.data.balance, reservedBalance: action.data.reservedBalance, @@ -6246,32 +6648,32 @@ const walletReducer = handleActions({ tipsBalance: action.data.tipsBalance }), - [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$h({}, state, { + [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$j({}, state, { checkingAddressOwnership: true }), - [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$h({}, state, { + [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$j({}, state, { checkingAddressOwnership: false }), [SET_DRAFT_TRANSACTION_AMOUNT]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$h({}, oldDraft, { amount: parseFloat(action.data.amount) }); + const newDraft = _extends$j({}, oldDraft, { amount: parseFloat(action.data.amount) }); - return _extends$h({}, state, { draftTransaction: newDraft }); + return _extends$j({}, state, { draftTransaction: newDraft }); }, [SET_DRAFT_TRANSACTION_ADDRESS]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$h({}, oldDraft, { address: action.data.address }); + const newDraft = _extends$j({}, oldDraft, { address: action.data.address }); - return _extends$h({}, state, { draftTransaction: newDraft }); + return _extends$j({}, state, { draftTransaction: newDraft }); }, [SEND_TRANSACTION_STARTED]: state => { - const newDraftTransaction = _extends$h({}, state.draftTransaction, { sending: true }); + const newDraftTransaction = _extends$j({}, state.draftTransaction, { sending: true }); - return _extends$h({}, state, { draftTransaction: newDraftTransaction }); + return _extends$j({}, state, { draftTransaction: newDraftTransaction }); }, [SEND_TRANSACTION_COMPLETED]: state => Object.assign({}, state, { @@ -6284,117 +6686,117 @@ const walletReducer = handleActions({ error: action.data.error }); - return _extends$h({}, state, { draftTransaction: newDraftTransaction }); + return _extends$j({}, state, { draftTransaction: newDraftTransaction }); }, - [SUPPORT_TRANSACTION_STARTED]: state => _extends$h({}, state, { + [SUPPORT_TRANSACTION_STARTED]: state => _extends$j({}, state, { sendingSupport: true }), - [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$h({}, state, { + [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$j({}, state, { sendingSupport: false }), - [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$h({}, state, { + [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$j({}, state, { error: action.data.error, sendingSupport: false }), - [CLEAR_SUPPORT_TRANSACTION]: state => _extends$h({}, state, { + [CLEAR_SUPPORT_TRANSACTION]: state => _extends$j({}, state, { sendingSupport: false }), - [WALLET_STATUS_COMPLETED]: (state, action) => _extends$h({}, state, { + [WALLET_STATUS_COMPLETED]: (state, action) => _extends$j({}, state, { walletIsEncrypted: action.result }), - [WALLET_ENCRYPT_START]: state => _extends$h({}, state, { + [WALLET_ENCRYPT_START]: state => _extends$j({}, state, { walletEncryptPending: true, walletEncryptSucceded: null, walletEncryptResult: null }), - [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$h({}, state, { + [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$j({}, state, { walletEncryptPending: false, walletEncryptSucceded: true, walletEncryptResult: action.result }), - [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$h({}, state, { + [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$j({}, state, { walletEncryptPending: false, walletEncryptSucceded: false, walletEncryptResult: action.result }), - [WALLET_DECRYPT_START]: state => _extends$h({}, state, { + [WALLET_DECRYPT_START]: state => _extends$j({}, state, { walletDecryptPending: true, walletDecryptSucceded: null, walletDecryptResult: null }), - [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$h({}, state, { + [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$j({}, state, { walletDecryptPending: false, walletDecryptSucceded: true, walletDecryptResult: action.result }), - [WALLET_DECRYPT_FAILED]: (state, action) => _extends$h({}, state, { + [WALLET_DECRYPT_FAILED]: (state, action) => _extends$j({}, state, { walletDecryptPending: false, walletDecryptSucceded: false, walletDecryptResult: action.result }), - [WALLET_UNLOCK_START]: state => _extends$h({}, state, { + [WALLET_UNLOCK_START]: state => _extends$j({}, state, { walletUnlockPending: true, walletUnlockSucceded: null, walletUnlockResult: null }), - [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$h({}, state, { + [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$j({}, state, { walletUnlockPending: false, walletUnlockSucceded: true, walletUnlockResult: action.result }), - [WALLET_UNLOCK_FAILED]: (state, action) => _extends$h({}, state, { + [WALLET_UNLOCK_FAILED]: (state, action) => _extends$j({}, state, { walletUnlockPending: false, walletUnlockSucceded: false, walletUnlockResult: action.result }), - [WALLET_LOCK_START]: state => _extends$h({}, state, { + [WALLET_LOCK_START]: state => _extends$j({}, state, { walletLockPending: false, walletLockSucceded: null, walletLockResult: null }), - [WALLET_LOCK_COMPLETED]: (state, action) => _extends$h({}, state, { + [WALLET_LOCK_COMPLETED]: (state, action) => _extends$j({}, state, { walletLockPending: false, walletLockSucceded: true, walletLockResult: action.result }), - [WALLET_LOCK_FAILED]: (state, action) => _extends$h({}, state, { + [WALLET_LOCK_FAILED]: (state, action) => _extends$j({}, state, { walletLockPending: false, walletLockSucceded: false, walletLockResult: action.result }), - [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$h({}, state, { + [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$j({}, state, { transactionListFilter: action.data }), - [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$h({}, state, { + [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$j({}, state, { latestBlock: action.data }), - [WALLET_RESTART]: state => _extends$h({}, state, { + [WALLET_RESTART]: state => _extends$j({}, state, { walletReconnecting: true }), - [WALLET_RESTART_COMPLETED]: state => _extends$h({}, state, { + [WALLET_RESTART_COMPLETED]: state => _extends$j({}, state, { walletReconnecting: false }) -}, defaultState$7); +}, defaultState$9); // @@ -6409,14 +6811,14 @@ const makeSelectContentPositionForUri = uri => reselect.createSelector(selectSta return state.positions[id] ? state.positions[id][outpoint] : null; }); -var _extends$i = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; +var _extends$k = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; const selectState$6 = state => state.notifications || {}; const selectToast = reselect.createSelector(selectState$6, state => { if (state.toasts.length) { const { id, params } = state.toasts[0]; - return _extends$i({ + return _extends$k({ id }, params); } @@ -6437,11 +6839,64 @@ const selectError = reselect.createSelector(selectState$6, state => { // -const selectState$7 = state => state.tags || {}; +const selectState$7 = state => state.comments || {}; -const selectKnownTagsByName = reselect.createSelector(selectState$7, state => state.knownTags); +const selectCommentsById = reselect.createSelector(selectState$7, state => state.commentById || {}); -const selectFollowedTagsList = reselect.createSelector(selectState$7, state => state.followedTags.filter(tag => typeof tag === 'string')); +const selectIsFetchingComments = reselect.createSelector(selectState$7, state => state.isLoading); + +const selectCommentsByClaimId = reselect.createSelector(selectState$7, selectCommentsById, (state, byId) => { + const byClaimId = state.byId || {}; + const comments = {}; + + // replace every comment_id in the list with the actual comment object + Object.keys(byClaimId).forEach(claimId => { + const commentIds = byClaimId[claimId]; + + comments[claimId] = Array(commentIds === null ? 0 : commentIds.length); + for (let i = 0; i < commentIds.length; i++) { + comments[claimId][i] = byId[commentIds[i]]; + } + }); + + return comments; +}); + +// previously this used a mapping from claimId -> Array +/* export const selectCommentsById = createSelector( + selectState, + state => state.byId || {} +); */ +const selectCommentsByUri = reselect.createSelector(selectState$7, state => { + const byUri = state.commentsByUri || {}; + const comments = {}; + Object.keys(byUri).forEach(uri => { + const claimId = byUri[uri]; + if (claimId === null) { + comments[uri] = null; + } else { + comments[uri] = claimId; + } + }); + + return comments; +}); + +const makeSelectCommentsForUri = uri => reselect.createSelector(selectCommentsByClaimId, selectCommentsByUri, (byClaimId, byUri) => { + const claimId = byUri[uri]; + return byClaimId && byClaimId[claimId]; +}); + +// todo: allow SDK to retrieve user comments through comment_list +// todo: implement selectors for selecting comments owned by user + +// + +const selectState$8 = state => state.tags || {}; + +const selectKnownTagsByName = reselect.createSelector(selectState$8, state => state.knownTags); + +const selectFollowedTagsList = reselect.createSelector(selectState$8, state => state.followedTags.filter(tag => typeof tag === 'string')); const selectFollowedTags = reselect.createSelector(selectFollowedTagsList, followedTags => followedTags.map(tag => ({ name: tag.toLowerCase() })).sort((a, b) => a.name.localeCompare(b.name))); @@ -6462,6 +6917,18 @@ const makeSelectIsFollowingTag = tag => reselect.createSelector(selectFollowedTa return followedTags.some(followedTag => followedTag.name === tag.toLowerCase()); }); +// + +const selectState$9 = state => state.blocked || {}; + +const selectBlockedChannels = reselect.createSelector(selectState$9, state => state.blockedChannels); + +const selectBlockedChannelsCount = reselect.createSelector(selectBlockedChannels, state => state.length); + +const selectChannelIsBlocked = uri => reselect.createSelector(selectBlockedChannels, state => { + return state.includes(uri); +}); + exports.ABANDON_STATES = abandon_states; exports.ACTIONS = action_types; exports.CLAIM_VALUES = claim; @@ -6484,9 +6951,11 @@ exports.TXO_LIST = txo_list; exports.TX_LIST = transaction_list; exports.apiCall = apiCall; exports.batchActions = batchActions; +exports.blockedReducer = blockedReducer; exports.buildSharedStateMiddleware = buildSharedStateMiddleware; exports.buildURI = buildURI; exports.claimsReducer = claimsReducer; +exports.commentReducer = commentReducer; exports.contentReducer = contentReducer; exports.convertToShareLink = convertToShareLink; exports.createNormalizedClaimSearchKey = createNormalizedClaimSearchKey; @@ -6505,6 +6974,11 @@ exports.doClearPublish = doClearPublish; exports.doClearPurchasedUriSuccess = doClearPurchasedUriSuccess; exports.doClearRepostError = doClearRepostError; exports.doClearSupport = doClearSupport; +exports.doCommentAbandon = doCommentAbandon; +exports.doCommentCreate = doCommentCreate; +exports.doCommentHide = doCommentHide; +exports.doCommentList = doCommentList; +exports.doCommentUpdate = doCommentUpdate; exports.doCreateChannel = doCreateChannel; exports.doDeleteTag = doDeleteTag; exports.doDismissError = doDismissError; @@ -6543,6 +7017,7 @@ exports.doSetFileListSort = doSetFileListSort; exports.doSetTransactionListFilter = doSetTransactionListFilter; exports.doSupportAbandonForClaim = doSupportAbandonForClaim; exports.doToast = doToast; +exports.doToggleBlockChannel = doToggleBlockChannel; exports.doToggleTagFollow = doToggleTagFollow; exports.doUpdateBalance = doUpdateBalance; exports.doUpdateBlockHeight = doUpdateBlockHeight; @@ -6574,6 +7049,7 @@ exports.makeSelectClaimIsPending = makeSelectClaimIsPending; exports.makeSelectClaimWasPurchased = makeSelectClaimWasPurchased; exports.makeSelectClaimsInChannelForCurrentPageState = makeSelectClaimsInChannelForCurrentPageState; exports.makeSelectClaimsInChannelForPage = makeSelectClaimsInChannelForPage; +exports.makeSelectCommentsForUri = makeSelectCommentsForUri; exports.makeSelectContentPositionForUri = makeSelectContentPositionForUri; exports.makeSelectContentTypeForUri = makeSelectContentTypeForUri; exports.makeSelectCoverForUri = makeSelectCoverForUri; @@ -6636,9 +7112,12 @@ exports.selectAllClaimsByChannel = selectAllClaimsByChannel; exports.selectAllFetchingChannelClaims = selectAllFetchingChannelClaims; exports.selectAllMyClaimsByOutpoint = selectAllMyClaimsByOutpoint; exports.selectBalance = selectBalance; +exports.selectBlockedChannels = selectBlockedChannels; +exports.selectBlockedChannelsCount = selectBlockedChannelsCount; exports.selectBlocks = selectBlocks; exports.selectChannelClaimCounts = selectChannelClaimCounts; exports.selectChannelImportPending = selectChannelImportPending; +exports.selectChannelIsBlocked = selectChannelIsBlocked; exports.selectClaimIdsByUri = selectClaimIdsByUri; exports.selectClaimSearchByQuery = selectClaimSearchByQuery; exports.selectClaimSearchByQueryLastPageReached = selectClaimSearchByQueryLastPageReached; @@ -6674,6 +7153,7 @@ exports.selectFollowedTagsList = selectFollowedTagsList; exports.selectGettingNewAddress = selectGettingNewAddress; exports.selectHasTransactions = selectHasTransactions; exports.selectIsFetchingClaimListMine = selectIsFetchingClaimListMine; +exports.selectIsFetchingComments = selectIsFetchingComments; exports.selectIsFetchingFileList = selectIsFetchingFileList; exports.selectIsFetchingFileListDownloadedOrPublished = selectIsFetchingFileListDownloadedOrPublished; exports.selectIsFetchingMyPurchases = selectIsFetchingMyPurchases; diff --git a/flow-typed/Blocklist.js b/flow-typed/Blocklist.js new file mode 100644 index 0000000..454a714 --- /dev/null +++ b/flow-typed/Blocklist.js @@ -0,0 +1,10 @@ +declare type BlocklistState = { + blockedChannels: Array +}; + +declare type BlocklistAction = { + type: string, + data: { + uri: string, + }, +}; diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js new file mode 100644 index 0000000..64ea974 --- /dev/null +++ b/flow-typed/Comment.js @@ -0,0 +1,23 @@ +declare type Comment = { + comment: string, // comment body + comment_id: string, // sha256 digest + claim_id: string, // id linking to the claim this comment + timestamp: number, // integer representing unix-time + is_hidden: boolean, // claim owner may enable/disable this + channel_id?: string, // claimId of channel signing this comment + channel_name?: string, // name of channel claim + channel_url?: string, // full lbry url to signing channel + signature?: string, // signature of comment by originating channel + signing_ts?: string, // timestamp used when signing this comment + is_channel_signature_valid?: boolean, // whether or not the signature could be validated + parent_id?: number, // comment_id of comment this is in reply to +}; + +// todo: relate individual comments to their commentId +declare type CommentsState = { + commentsByUri: { [string]: string }, + byId: { [string]: Array }, + commentById: { [string]: Comment }, + isLoading: boolean, + myComments: ?Set, +}; diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 3525f4d..b14cfb8 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -131,6 +131,23 @@ export const PURCHASE_LIST_STARTED = 'PURCHASE_LIST_STARTED'; export const PURCHASE_LIST_COMPLETED = 'PURCHASE_LIST_COMPLETED'; export const PURCHASE_LIST_FAILED = 'PURCHASE_LIST_FAILED'; +// Comments +export const COMMENT_LIST_STARTED = 'COMMENT_LIST_STARTED'; +export const COMMENT_LIST_COMPLETED = 'COMMENT_LIST_COMPLETED'; +export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED'; +export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED'; +export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED'; +export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; +export const COMMENT_ABANDON_STARTED = 'COMMENT_ABANDON_STARTED'; +export const COMMENT_ABANDON_COMPLETED = 'COMMENT_ABANDON_COMPLETED'; +export const COMMENT_ABANDON_FAILED = 'COMMENT_ABANDON_FAILED'; +export const COMMENT_UPDATE_STARTED = 'COMMENT_UPDATE_STARTED'; +export const COMMENT_UPDATE_COMPLETED = 'COMMENT_UPDATE_COMPLETED'; +export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED'; +export const COMMENT_HIDE_STARTED = 'COMMENT_HIDE_STARTED'; +export const COMMENT_HIDE_COMPLETED = 'COMMENT_HIDE_COMPLETED'; +export const COMMENT_HIDE_FAILED = 'COMMENT_HIDE_FAILED'; + // Files export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'; export const FILE_LIST_SUCCEEDED = 'FILE_LIST_SUCCEEDED'; @@ -266,5 +283,8 @@ export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW'; export const TAG_ADD = 'TAG_ADD'; export const TAG_DELETE = 'TAG_DELETE'; +// Blocked Channels +export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; + // Sync export const USER_STATE_POPULATE = 'USER_STATE_POPULATE'; diff --git a/src/index.js b/src/index.js index d28f38f..dcec873 100644 --- a/src/index.js +++ b/src/index.js @@ -135,6 +135,16 @@ export { export { doToggleTagFollow, doAddTag, doDeleteTag } from 'redux/actions/tags'; +export { + doCommentList, + doCommentCreate, + doCommentAbandon, + doCommentHide, + doCommentUpdate, +} from 'redux/actions/comments'; + +export { doToggleBlockChannel } from 'redux/actions/blocked'; + export { doPopulateSharedUserState, doPreferenceGet, doPreferenceSet } from 'redux/actions/sync'; // utils @@ -145,12 +155,14 @@ export { isClaimNsfw, createNormalizedClaimSearchKey } from 'util/claim'; // reducers export { claimsReducer } from 'redux/reducers/claims'; +export { commentReducer } from 'redux/reducers/comments'; export { contentReducer } from 'redux/reducers/content'; export { fileInfoReducer } from 'redux/reducers/file_info'; export { notificationsReducer } from 'redux/reducers/notifications'; export { publishReducer } from 'redux/reducers/publish'; export { searchReducer } from 'redux/reducers/search'; export { tagsReducer } from 'redux/reducers/tags'; +export { blockedReducer } from 'redux/reducers/blocked'; export { walletReducer } from 'redux/reducers/wallet'; // selectors @@ -240,6 +252,8 @@ export { selectPurchaseUriSuccess, } from 'redux/selectors/claims'; +export { makeSelectCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments'; + export { makeSelectFileInfoForUri, makeSelectDownloadingForUri, @@ -347,3 +361,9 @@ export { selectUnfollowedTags, makeSelectIsFollowingTag, } from 'redux/selectors/tags'; + +export { + selectBlockedChannels, + selectChannelIsBlocked, + selectBlockedChannelsCount, +} from 'redux/selectors/blocked'; diff --git a/src/redux/actions/blocked.js b/src/redux/actions/blocked.js new file mode 100644 index 0000000..1e96563 --- /dev/null +++ b/src/redux/actions/blocked.js @@ -0,0 +1,9 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; + +export const doToggleBlockChannel = (uri: string) => ({ + type: ACTIONS.TOGGLE_BLOCK_CHANNEL, + data: { + uri, + }, +}); diff --git a/src/redux/actions/comments.js b/src/redux/actions/comments.js new file mode 100644 index 0000000..22b38e4 --- /dev/null +++ b/src/redux/actions/comments.js @@ -0,0 +1,225 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import Lbry from 'lbry'; +import { selectClaimsByUri, selectMyChannelClaims } from 'redux/selectors/claims'; +import { doToast } from 'redux/actions/notifications'; + +export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) { + return (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const claim = selectClaimsByUri(state)[uri]; + const claimId = claim ? claim.claim_id : null; + + dispatch({ + type: ACTIONS.COMMENT_LIST_STARTED, + }); + Lbry.comment_list({ + claim_id: claimId, + page, + page_size: pageSize, + }) + .then((result: CommentListResponse) => { + const { items: comments } = result; + dispatch({ + type: ACTIONS.COMMENT_LIST_COMPLETED, + data: { + comments, + claimId: claimId, + uri: uri, + }, + }); + }) + .catch(error => { + console.log(error); + dispatch({ + type: ACTIONS.COMMENT_LIST_FAILED, + data: error, + }); + }); + }; +} + +export function doCommentCreate( + comment: string = '', + claim_id: string = '', + channel: string, + parent_id?: string +) { + return (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + dispatch({ + type: ACTIONS.COMMENT_CREATE_STARTED, + }); + + const myChannels = selectMyChannelClaims(state); + const namedChannelClaim = + myChannels && myChannels.find(myChannel => myChannel.name === channel); + const channel_id = namedChannelClaim.claim_id; + + if (channel_id == null) { + dispatch({ + type: ACTIONS.COMMENT_CREATE_FAILED, + data: {}, + }); + dispatch( + doToast({ + message: 'Channel cannot be anonymous, please select a channel and try again.', + isError: true, + }) + ); + return; + } + + return Lbry.comment_create({ + comment: comment, + claim_id: claim_id, + channel_id: channel_id, + parent_id: parent_id, + }) + .then((result: CommentCreateResponse) => { + dispatch({ + type: ACTIONS.COMMENT_CREATE_COMPLETED, + data: { + comment: result, + claimId: claim_id, + }, + }); + }) + .catch(error => { + dispatch({ + type: ACTIONS.COMMENT_CREATE_FAILED, + data: error, + }); + dispatch( + doToast({ + message: 'Unable to create comment, please try again later.', + isError: true, + }) + ); + }); + }; +} + +export function doCommentHide(comment_id: string) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.COMMENT_HIDE_STARTED, + }); + return Lbry.comment_hide({ + comment_ids: [comment_id], + }) + .then((result: CommentHideResponse) => { + dispatch({ + type: ACTIONS.COMMENT_HIDE_COMPLETED, + data: result, + }); + }) + .catch(error => { + dispatch({ + type: ACTIONS.COMMENT_HIDE_FAILED, + data: error, + }); + dispatch( + doToast({ + message: 'Unable to hide this comment, please try again later.', + isError: true, + }) + ); + }); + }; +} + +export function doCommentAbandon(comment_id: string) { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.COMMENT_ABANDON_STARTED, + }); + return Lbry.comment_abandon({ + comment_id: comment_id, + }) + .then((result: CommentAbandonResponse) => { + // Comment may not be deleted if the signing channel can't be signed. + // This will happen if the channel was recently created or abandoned. + if (result.abandoned) { + dispatch({ + type: ACTIONS.COMMENT_ABANDON_COMPLETED, + data: { + comment_id: comment_id, + }, + }); + } else { + dispatch({ + type: ACTIONS.COMMENT_ABANDON_FAILED, + }); + dispatch( + doToast({ + message: 'Your channel is still being setup, try again in a few moments.', + isError: true, + }) + ); + } + }) + .catch(error => { + dispatch({ + type: ACTIONS.COMMENT_ABANDON_FAILED, + data: error, + }); + dispatch( + doToast({ + message: 'Unable to delete this comment, please try again later.', + isError: true, + }) + ); + }); + }; +} + +export function doCommentUpdate(comment_id: string, comment: string) { + // if they provided an empty string, they must have wanted to abandon + if (comment === '') { + return doCommentAbandon(comment_id); + } else { + return (dispatch: Dispatch) => { + dispatch({ + type: ACTIONS.COMMENT_UPDATE_STARTED, + }); + return Lbry.comment_update({ + comment_id: comment_id, + comment: comment, + }) + .then((result: CommentUpdateResponse) => { + if (result != null) { + dispatch({ + type: ACTIONS.COMMENT_UPDATE_COMPLETED, + data: { + comment: result, + }, + }); + } else { + // the result will return null + dispatch({ + type: ACTIONS.COMMENT_UPDATE_FAILED, + }); + dispatch( + doToast({ + message: 'Your channel is still being setup, try again in a few moments.', + isError: true, + }) + ); + } + }) + .catch(error => { + dispatch({ + type: ACTIONS.COMMENT_UPDATE_FAILED, + data: error, + }); + dispatch( + doToast({ + message: 'Unable to edit this comment, please try again later.', + isError: true, + }) + ); + }); + }; + } +} diff --git a/src/redux/reducers/blocked.js b/src/redux/reducers/blocked.js new file mode 100644 index 0000000..c688936 --- /dev/null +++ b/src/redux/reducers/blocked.js @@ -0,0 +1,41 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const defaultState: BlocklistState = { + blockedChannels: [], +}; + +export const blockedReducer = handleActions( + { + [ACTIONS.TOGGLE_BLOCK_CHANNEL]: ( + state: BlocklistState, + action: BlocklistAction + ): BlocklistState => { + const { blockedChannels } = state; + const { uri } = action.data; + let newBlockedChannels = blockedChannels.slice(); + + if (newBlockedChannels.includes(uri)) { + newBlockedChannels = newBlockedChannels.filter(id => id !== uri); + } else { + newBlockedChannels.push(uri); + } + + return { + blockedChannels: newBlockedChannels, + }; + }, + [ACTIONS.USER_STATE_POPULATE]: ( + state: BlocklistState, + action: { data: { blocked: ?Array } } + ) => { + const { blocked } = action.data; + return { + ...state, + blockedChannels: blocked && blocked.length ? blocked : state.blockedChannels, + }; + }, + }, + defaultState +); diff --git a/src/redux/reducers/comments.js b/src/redux/reducers/comments.js new file mode 100644 index 0000000..46d08d8 --- /dev/null +++ b/src/redux/reducers/comments.js @@ -0,0 +1,153 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const defaultState: CommentsState = { + commentById: {}, // commentId -> Comment + byId: {}, // ClaimID -> list of comments + commentsByUri: {}, // URI -> claimId + isLoading: false, + myComments: undefined, +}; + +export const commentReducer = handleActions( + { + [ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({ + ...state, + isLoading: true, + }), + + [ACTIONS.COMMENT_CREATE_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: false, + }), + + [ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { + const { comment, claimId }: { comment: Comment, claimId: string } = action.data; + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + const comments = byId[claimId]; + const newCommentIds = comments.slice(); + + // add the comment by its ID + commentById[comment.comment_id] = comment; + + // push the comment_id to the top of ID list + newCommentIds.unshift(comment.comment_id); + byId[claimId] = newCommentIds; + + return { + ...state, + commentById, + byId, + isLoading: false, + }; + }, + + [ACTIONS.COMMENT_LIST_STARTED]: state => ({ ...state, isLoading: true }), + + [ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => { + const { comments, claimId, uri } = action.data; + + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + const commentsByUri = Object.assign({}, state.commentsByUri); + + if (comments) { + // we use an Array to preserve order of listing + // in reality this doesn't matter and we can just + // sort comments by their timestamp + const commentIds = Array(comments.length); + + // map the comment_ids to the new comments + for (let i = 0; i < comments.length; i++) { + commentIds[i] = comments[i].comment_id; + commentById[commentIds[i]] = comments[i]; + } + + byId[claimId] = commentIds; + commentsByUri[uri] = claimId; + } + return { + ...state, + byId, + commentById, + commentsByUri, + isLoading: false, + }; + }, + + [ACTIONS.COMMENT_LIST_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: false, + }), + [ACTIONS.COMMENT_ABANDON_STARTED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: true, + }), + [ACTIONS.COMMENT_ABANDON_COMPLETED]: (state: CommentsState, action: any) => { + const { comment_id } = action.data; + const commentById = Object.assign({}, state.commentById); + const byId = Object.assign({}, state.byId); + + // to remove the comment and its references + const claimId = commentById[comment_id].claim_id; + for (let i = 0; i < byId[claimId].length; i++) { + if (byId[claimId][i] === comment_id) { + byId[claimId].splice(i, 1); + break; + } + } + delete commentById[comment_id]; + + return { + ...state, + commentById, + byId, + isLoading: false, + }; + }, + // do nothing + [ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: false, + }), + // do nothing + [ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: true, + }), + // replace existing comment with comment returned here under its comment_id + [ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => { + const { comment } = action.data; + const commentById = Object.assign({}, state.commentById); + commentById[comment.comment_id] = comment; + + return { + ...state, + commentById, + isLoading: false, + }; + }, + // nothing can be done here + [ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: false, + }), + // nothing can really be done here + [ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: true, + }), + [ACTIONS.COMMENT_HIDE_COMPLETED]: (state: CommentsState, action: any) => ({ + ...state, // todo: add HiddenComments state & create selectors + isLoading: false, + }), + // nothing can be done here + [ACTIONS.COMMENT_HIDE_FAILED]: (state: CommentsState, action: any) => ({ + ...state, + isLoading: false, + }), + }, + defaultState +); diff --git a/src/redux/selectors/blocked.js b/src/redux/selectors/blocked.js new file mode 100644 index 0000000..424ea89 --- /dev/null +++ b/src/redux/selectors/blocked.js @@ -0,0 +1,22 @@ +// @flow +import { createSelector } from 'reselect'; + +const selectState = (state: { blocked: BlocklistState }) => state.blocked || {}; + +export const selectBlockedChannels = createSelector( + selectState, + (state: BlocklistState) => state.blockedChannels +); + +export const selectBlockedChannelsCount = createSelector( + selectBlockedChannels, + (state: Array) => state.length +); + +export const selectChannelIsBlocked = (uri: string) => + createSelector( + selectBlockedChannels, + (state: Array) => { + return state.includes(uri); + } + ); diff --git a/src/redux/selectors/comments.js b/src/redux/selectors/comments.js new file mode 100644 index 0000000..d8033ff --- /dev/null +++ b/src/redux/selectors/comments.js @@ -0,0 +1,71 @@ +// @flow +import { createSelector } from 'reselect'; + +const selectState = state => state.comments || {}; + +export const selectCommentsById = createSelector( + selectState, + state => state.commentById || {} +); + +export const selectIsFetchingComments = createSelector( + selectState, + state => state.isLoading +); + +export const selectCommentsByClaimId = createSelector( + selectState, + selectCommentsById, + (state, byId) => { + const byClaimId = state.byId || {}; + const comments = {}; + + // replace every comment_id in the list with the actual comment object + Object.keys(byClaimId).forEach(claimId => { + const commentIds = byClaimId[claimId]; + + comments[claimId] = Array(commentIds === null ? 0 : commentIds.length); + for (let i = 0; i < commentIds.length; i++) { + comments[claimId][i] = byId[commentIds[i]]; + } + }); + + return comments; + } +); + +// previously this used a mapping from claimId -> Array +/* export const selectCommentsById = createSelector( + selectState, + state => state.byId || {} +); */ +export const selectCommentsByUri = createSelector( + selectState, + state => { + const byUri = state.commentsByUri || {}; + const comments = {}; + Object.keys(byUri).forEach(uri => { + const claimId = byUri[uri]; + if (claimId === null) { + comments[uri] = null; + } else { + comments[uri] = claimId; + } + }); + + return comments; + } +); + +export const makeSelectCommentsForUri = (uri: string) => + createSelector( + selectCommentsByClaimId, + selectCommentsByUri, + (byClaimId, byUri) => { + const claimId = byUri[uri]; + return byClaimId && byClaimId[claimId]; + } + ); + +// todo: allow SDK to retrieve user comments through comment_list +// todo: implement selectors for selecting comments owned by user