diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 3f58baa..8d472d8 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -135,6 +135,15 @@ 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'; @@ -382,6 +391,15 @@ var action_types = /*#__PURE__*/Object.freeze({ 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, @@ -782,12 +800,12 @@ var daemon_settings = /*#__PURE__*/Object.freeze({ }); /* -* How to use this file: -* Settings exported from here will trigger the setting to be -* sent to the preference middleware when set using the -* usual setDaemonSettings and clearDaemonSettings methods. -* -* See redux/settings/actions in the app for where this is used. + * How to use this file: + * Settings exported from here will trigger the setting to be + * sent to the preference middleware when set using the + * usual setDaemonSettings and clearDaemonSettings methods. + * + * See redux/settings/actions in the app for where this is used. */ const WALLET_SERVERS = LBRYUM_SERVERS; @@ -934,6 +952,11 @@ const Lbry = { // Comments comment_list: (params = {}) => daemonCallWithResult('comment_list', params), comment_create: (params = {}) => daemonCallWithResult('comment_create', params), + comment_hide: (params = {}) => daemonCallWithResult('comment_hide', params), + comment_abandon: (params = {}) => daemonCallWithResult('comment_abandon', params), + // requires SDK ver. 0.53.0 + comment_update: (params = {}) => daemonCallWithResult('comment_update', params), + // Connect to the sdk connect: () => { if (Lbry.connectPromise === null) { @@ -1241,6 +1264,18 @@ function buildURI(UrlObj, includeProto = true, protoDefault = 'lbry://') { deprecatedParts = _objectWithoutProperties(UrlObj, ['streamName', 'streamClaimId', 'channelName', 'channelClaimId', 'primaryClaimSequence', 'primaryBidPosition', 'secondaryClaimSequence', 'secondaryBidPosition']); const { claimId, claimName, contentName } = deprecatedParts; + { + if (claimId) { + console.error(__("'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead")); + } + if (claimName) { + console.error(__("'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead")); + } + if (contentName) { + console.error(__("'contentName' should no longer be used. Use 'streamName' instead")); + } + } + if (!claimName && !channelName && !streamName) { console.error(__("'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.")); } @@ -1565,7 +1600,10 @@ function extractUserState(rawObj) { function doPopulateSharedUserState(sharedSettings) { return dispatch => { const { subscriptions, tags, blocked, settings } = extractUserState(sharedSettings); - dispatch({ type: USER_STATE_POPULATE, data: { subscriptions, tags, blocked, settings } }); + dispatch({ + type: USER_STATE_POPULATE, + data: { subscriptions, tags, blocked, settings } + }); }; } @@ -4146,6 +4184,8 @@ const doUpdateSearchOptions = newOptions => (dispatch, getState) => { } }; +// + function savePosition(claimId, outpoint, position) { return dispatch => { dispatch({ @@ -4223,9 +4263,10 @@ function doCommentCreate(comment = '', claim_id = '', channel, parent_id) { const namedChannelClaim = myChannels && myChannels.find(myChannel => myChannel.name === channel); const channel_id = namedChannelClaim ? namedChannelClaim.claim_id : null; return lbryProxy.comment_create({ - comment, - claim_id, - channel_id + comment: comment, + claim_id: claim_id, + channel_id: channel_id, + parent_id: parent_id }).then(result => { dispatch({ type: COMMENT_CREATE_COMPLETED, @@ -4247,6 +4288,96 @@ function doCommentCreate(comment = '', claim_id = '', channel, parent_id) { }; } +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: 'There was an error hiding 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 + }); + } + }).catch(error => { + dispatch({ + type: COMMENT_ABANDON_FAILED, + data: error + }); + dispatch(doToast({ + message: 'There was an error hiding 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 => { + dispatch({ + type: COMMENT_UPDATE_COMPLETED, + data: { + comment: result + } + }); + }).catch(error => { + dispatch({ type: COMMENT_UPDATE_FAILED, data: error }); + dispatch(doToast({ + message: 'There was an error hiding this comment. Please try again later.', + isError: true + })); + }); + }; + } +} + // const doToggleBlockChannel = uri => ({ @@ -4663,9 +4794,11 @@ const handleActions = (actionMap, defaultState) => (state = defaultState, action var _extends$8 = 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$1 = { - byId: {}, - commentsByUri: {}, - isLoading: false + commentById: {}, // commentId -> Comment + byId: {}, // ClaimID -> list of comments + commentsByUri: {}, // URI -> claimId + isLoading: false, + myComments: undefined }; const commentReducer = handleActions({ @@ -4679,15 +4812,22 @@ const commentReducer = handleActions({ [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 newComments = comments.slice(); + const newCommentIds = comments.slice(); - newComments.unshift(comment); - byId[claimId] = newComments; + // 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$8({}, state, { - byId + commentById, + byId, + isLoading: false }); }, @@ -4695,15 +4835,29 @@ const commentReducer = handleActions({ [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) { - byId[claimId] = 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$8({}, state, { byId, + commentById, commentsByUri, isLoading: false }); @@ -4711,6 +4865,67 @@ const commentReducer = handleActions({ [COMMENT_LIST_FAILED]: (state, action) => _extends$8({}, state, { isLoading: false + }), + [COMMENT_ABANDON_STARTED]: (state, action) => _extends$8({}, 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$8({}, state, { + commentById, + byId, + isLoading: false + }); + }, + // do nothing + [COMMENT_ABANDON_FAILED]: (state, action) => _extends$8({}, state, { + isLoading: false + }), + // do nothing + [COMMENT_UPDATE_STARTED]: (state, action) => _extends$8({}, 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); + + if (comment) { + commentById[comment.comment_id] = comment; + } + + return _extends$8({}, state, { + commentById, + isLoading: false + }); + }, + // nothing can be done here + [COMMENT_UPDATE_FAILED]: (state, action) => _extends$8({}, state, { + isLoading: false + }), + // nothing can really be done here + [COMMENT_HIDE_STARTED]: (state, action) => _extends$8({}, state, { + isLoading: true + }), + [COMMENT_HIDE_COMPLETED]: (state, action) => _extends$8({}, state, { // todo: add HiddenComments state & create selectors + isLoading: false + }), + // nothing can be done here + [COMMENT_HIDE_FAILED]: (state, action) => _extends$8({}, state, { + isLoading: false }) }, defaultState$1); @@ -5581,6 +5796,8 @@ const walletReducer = handleActions({ }) }, defaultState$a); +// + const selectState$6 = state => state.content || {}; const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$6, makeSelectClaimForUri(uri), (state, claim) => { @@ -5622,8 +5839,30 @@ const selectError = reselect.createSelector(selectState$7, state => { const selectState$8 = state => state.comments || {}; -const selectCommentsById = reselect.createSelector(selectState$8, state => state.byId || {}); +const selectCommentsById = reselect.createSelector(selectState$8, state => state.commentById || {}); +const selectCommentsByClaimId = reselect.createSelector(selectState$8, 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$8, state => { const byUri = state.commentsByUri || {}; const comments = {}; @@ -5635,14 +5874,18 @@ const selectCommentsByUri = reselect.createSelector(selectState$8, state => { comments[uri] = claimId; } }); + return comments; }); -const makeSelectCommentsForUri = uri => reselect.createSelector(selectCommentsById, selectCommentsByUri, (byId, byUri) => { +const makeSelectCommentsForUri = uri => reselect.createSelector(selectCommentsByClaimId, selectCommentsByUri, (byClaimId, byUri) => { const claimId = byUri[uri]; - return byId && byId[claimId]; + 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$9 = state => state.tags || {}; @@ -5720,8 +5963,11 @@ exports.doCheckPendingPublishes = doCheckPendingPublishes; exports.doClaimSearch = doClaimSearch; exports.doClearPublish = doClearPublish; exports.doClearSupport = doClearSupport; +exports.doCommentAbandon = doCommentAbandon; exports.doCommentCreate = doCommentCreate; +exports.doCommentHide = doCommentHide; exports.doCommentList = doCommentList; +exports.doCommentUpdate = doCommentUpdate; exports.doCreateChannel = doCreateChannel; exports.doDeletePurchasedUri = doDeletePurchasedUri; exports.doDeleteTag = doDeleteTag; diff --git a/dist/flow-typed/Comment.js b/dist/flow-typed/Comment.js index f2f35b7..64ea974 100644 --- a/dist/flow-typed/Comment.js +++ b/dist/flow-typed/Comment.js @@ -1,19 +1,23 @@ declare type Comment = { - author?: string, - author_url?: string, - claim_index?: number, - comment_id?: number, - downvotes?: number, - message: string, - omitted?: number, - reply_count?: number, - time_posted?: number, - upvotes?: number, - parent_id?: number, + 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 = { - byId: {}, - isLoading: boolean, commentsByUri: { [string]: string }, -} + byId: { [string]: Array }, + commentById: { [string]: Comment }, + isLoading: boolean, + myComments: ?Set, +}; diff --git a/dist/flow-typed/Lbry.js b/dist/flow-typed/Lbry.js index cc8a4ac..d2657cb 100644 --- a/dist/flow-typed/Lbry.js +++ b/dist/flow-typed/Lbry.js @@ -125,6 +125,8 @@ declare type ChannelUpdateResponse = GenericTxResponse & { }; declare type CommentCreateResponse = Comment; +declare type CommentUpdateResponse = Comment; + declare type CommentListResponse = { items: Array, page: number, @@ -133,6 +135,16 @@ declare type CommentListResponse = { total_pages: number, }; +declare type CommentHideResponse = { + // keyed by the CommentIds entered + [string]: { hidden: boolean }, +}; + +declare type CommentAbandonResponse = { + // keyed by the CommentId given + abandoned: boolean, +}; + declare type ChannelListResponse = { items: Array, page: number, @@ -242,6 +254,10 @@ declare type LbryTypes = { // Commenting comment_list: (params: {}) => Promise, comment_create: (params: {}) => Promise, + comment_update: (params: {}) => Promise, + comment_hide: (params: {}) => Promise, + comment_abandon: (params: {}) => Promise, + // Wallet utilities wallet_balance: (params: {}) => Promise, wallet_decrypt: (prams: {}) => Promise, diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index f2f35b7..64ea974 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -1,19 +1,23 @@ declare type Comment = { - author?: string, - author_url?: string, - claim_index?: number, - comment_id?: number, - downvotes?: number, - message: string, - omitted?: number, - reply_count?: number, - time_posted?: number, - upvotes?: number, - parent_id?: number, + 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 = { - byId: {}, - isLoading: boolean, commentsByUri: { [string]: string }, -} + byId: { [string]: Array }, + commentById: { [string]: Comment }, + isLoading: boolean, + myComments: ?Set, +}; diff --git a/flow-typed/Lbry.js b/flow-typed/Lbry.js index cc8a4ac..d2657cb 100644 --- a/flow-typed/Lbry.js +++ b/flow-typed/Lbry.js @@ -125,6 +125,8 @@ declare type ChannelUpdateResponse = GenericTxResponse & { }; declare type CommentCreateResponse = Comment; +declare type CommentUpdateResponse = Comment; + declare type CommentListResponse = { items: Array, page: number, @@ -133,6 +135,16 @@ declare type CommentListResponse = { total_pages: number, }; +declare type CommentHideResponse = { + // keyed by the CommentIds entered + [string]: { hidden: boolean }, +}; + +declare type CommentAbandonResponse = { + // keyed by the CommentId given + abandoned: boolean, +}; + declare type ChannelListResponse = { items: Array, page: number, @@ -242,6 +254,10 @@ declare type LbryTypes = { // Commenting comment_list: (params: {}) => Promise, comment_create: (params: {}) => Promise, + comment_update: (params: {}) => Promise, + comment_hide: (params: {}) => Promise, + comment_abandon: (params: {}) => Promise, + // Wallet utilities wallet_balance: (params: {}) => Promise, wallet_decrypt: (prams: {}) => Promise, diff --git a/src/constants/action_types.js b/src/constants/action_types.js index 42cf987..1f8b841 100644 --- a/src/constants/action_types.js +++ b/src/constants/action_types.js @@ -111,6 +111,15 @@ 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'; diff --git a/src/constants/shared_preferences.js b/src/constants/shared_preferences.js index 56faf8f..e508e83 100644 --- a/src/constants/shared_preferences.js +++ b/src/constants/shared_preferences.js @@ -1,10 +1,10 @@ /* -* How to use this file: -* Settings exported from here will trigger the setting to be -* sent to the preference middleware when set using the -* usual setDaemonSettings and clearDaemonSettings methods. -* -* See redux/settings/actions in the app for where this is used. + * How to use this file: + * Settings exported from here will trigger the setting to be + * sent to the preference middleware when set using the + * usual setDaemonSettings and clearDaemonSettings methods. + * + * See redux/settings/actions in the app for where this is used. */ import * as DAEMON_SETTINGS from './daemon_settings'; diff --git a/src/index.js b/src/index.js index b9d554f..3650efd 100644 --- a/src/index.js +++ b/src/index.js @@ -122,7 +122,13 @@ export { export { doToggleTagFollow, doAddTag, doDeleteTag } from 'redux/actions/tags'; -export { doCommentList, doCommentCreate } from 'redux/actions/comments'; +export { + doCommentList, + doCommentCreate, + doCommentAbandon, + doCommentHide, + doCommentUpdate, +} from 'redux/actions/comments'; export { doToggleBlockChannel } from 'redux/actions/blocked'; diff --git a/src/lbry.js b/src/lbry.js index ceb96f3..f6cc88c 100644 --- a/src/lbry.js +++ b/src/lbry.js @@ -119,6 +119,11 @@ const Lbry: LbryTypes = { // Comments comment_list: (params = {}) => daemonCallWithResult('comment_list', params), comment_create: (params = {}) => daemonCallWithResult('comment_create', params), + comment_hide: (params = {}) => daemonCallWithResult('comment_hide', params), + comment_abandon: (params = {}) => daemonCallWithResult('comment_abandon', params), + // requires SDK ver. 0.53.0 + comment_update: (params = {}) => daemonCallWithResult('comment_update', params), + // Connect to the sdk connect: () => { if (Lbry.connectPromise === null) { diff --git a/src/redux/actions/comments.js b/src/redux/actions/comments.js index dbf1d94..4441651 100644 --- a/src/redux/actions/comments.js +++ b/src/redux/actions/comments.js @@ -43,7 +43,7 @@ export function doCommentCreate( comment: string = '', claim_id: string = '', channel: ?string, - parent_id?: number + parent_id?: string ) { return (dispatch: Dispatch, getState: GetState) => { const state = getState(); @@ -55,11 +55,12 @@ export function doCommentCreate( myChannels && myChannels.find(myChannel => myChannel.name === channel); const channel_id = namedChannelClaim ? namedChannelClaim.claim_id : null; return Lbry.comment_create({ - comment, - claim_id, - channel_id, + comment: comment, + claim_id: claim_id, + channel_id: channel_id, + parent_id: parent_id, }) - .then((result: Comment) => { + .then((result: CommentCreateResponse) => { dispatch({ type: ACTIONS.COMMENT_CREATE_COMPLETED, data: { @@ -82,3 +83,105 @@ export function doCommentCreate( }); }; } + +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: 'There was an error hiding 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, + }); + } + }) + .catch(error => { + dispatch({ + type: ACTIONS.COMMENT_ABANDON_FAILED, + data: error, + }); + dispatch( + doToast({ + message: 'There was an error hiding 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) => { + dispatch({ + type: ACTIONS.COMMENT_UPDATE_COMPLETED, + data: { + comment: result, + }, + }); + }) + .catch(error => { + dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED, data: error }); + dispatch( + doToast({ + message: 'There was an error hiding this comment. Please try again later.', + isError: true, + }) + ); + }); + }; + } +} diff --git a/src/redux/actions/content.js b/src/redux/actions/content.js index fff0f11..2654f74 100644 --- a/src/redux/actions/content.js +++ b/src/redux/actions/content.js @@ -1,7 +1,8 @@ +// @flow import * as ACTIONS from 'constants/action_types'; export function savePosition(claimId: string, outpoint: string, position: number) { - return dispatch => { + return (dispatch: Dispatch) => { dispatch({ type: ACTIONS.SET_CONTENT_POSITION, data: { claimId, outpoint, position }, diff --git a/src/redux/actions/sync.js b/src/redux/actions/sync.js index 41a25ea..7f80d33 100644 --- a/src/redux/actions/sync.js +++ b/src/redux/actions/sync.js @@ -14,7 +14,7 @@ type SharedData = { function extractUserState(rawObj: SharedData) { if (rawObj && rawObj.version === '0.1' && rawObj.value) { - const { subscriptions, tags, blocked, settings} = rawObj.value; + const { subscriptions, tags, blocked, settings } = rawObj.value; return { ...(subscriptions ? { subscriptions } : {}), @@ -30,7 +30,10 @@ function extractUserState(rawObj: SharedData) { export function doPopulateSharedUserState(sharedSettings: any) { return (dispatch: Dispatch) => { const { subscriptions, tags, blocked, settings } = extractUserState(sharedSettings); - dispatch({ type: ACTIONS.USER_STATE_POPULATE, data: { subscriptions, tags, blocked, settings } }); + dispatch({ + type: ACTIONS.USER_STATE_POPULATE, + data: { subscriptions, tags, blocked, settings }, + }); }; } diff --git a/src/redux/reducers/blocked.js b/src/redux/reducers/blocked.js index b69d9c9..c688936 100644 --- a/src/redux/reducers/blocked.js +++ b/src/redux/reducers/blocked.js @@ -33,8 +33,7 @@ export const blockedReducer = handleActions( const { blocked } = action.data; return { ...state, - blockedChannels: - blocked && blocked.length ? blocked : state.blockedChannels, + blockedChannels: blocked && blocked.length ? blocked : state.blockedChannels, }; }, }, diff --git a/src/redux/reducers/comments.js b/src/redux/reducers/comments.js index 3d1738b..493d225 100644 --- a/src/redux/reducers/comments.js +++ b/src/redux/reducers/comments.js @@ -3,9 +3,11 @@ import * as ACTIONS from 'constants/action_types'; import { handleActions } from 'util/redux-utils'; const defaultState: CommentsState = { - byId: {}, - commentsByUri: {}, + commentById: {}, // commentId -> Comment + byId: {}, // ClaimID -> list of comments + commentsByUri: {}, // URI -> claimId isLoading: false, + myComments: undefined, }; export const commentReducer = handleActions( @@ -21,17 +23,24 @@ export const commentReducer = handleActions( }), [ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { - const { comment, claimId }: any = action.data; + 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 newComments = comments.slice(); + const newCommentIds = comments.slice(); - newComments.unshift(comment); - byId[claimId] = newComments; + // 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, }; }, @@ -39,16 +48,30 @@ export const commentReducer = handleActions( [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) { - byId[claimId] = 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, }; @@ -58,6 +81,76 @@ export const commentReducer = handleActions( ...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); + + if (comment) { + 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/comments.js b/src/redux/selectors/comments.js index c391a5c..c3a2f2d 100644 --- a/src/redux/selectors/comments.js +++ b/src/redux/selectors/comments.js @@ -5,9 +5,35 @@ const selectState = state => state.comments || {}; export const selectCommentsById = createSelector( selectState, - state => state.byId || {} + state => state.commentById || {} ); +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 => { @@ -21,16 +47,20 @@ export const selectCommentsByUri = createSelector( comments[uri] = claimId; } }); + return comments; } ); export const makeSelectCommentsForUri = (uri: string) => createSelector( - selectCommentsById, + selectCommentsByClaimId, selectCommentsByUri, - (byId, byUri) => { + (byClaimId, byUri) => { const claimId = byUri[uri]; - return byId && byId[claimId]; + return byClaimId && byClaimId[claimId]; } ); + +// todo: allow SDK to retrieve user comments through comment_list +// todo: implement selectors for selecting comments owned by user diff --git a/src/redux/selectors/content.js b/src/redux/selectors/content.js index f494fab..2daac6e 100644 --- a/src/redux/selectors/content.js +++ b/src/redux/selectors/content.js @@ -1,14 +1,19 @@ +// @flow import { createSelector } from 'reselect'; import { makeSelectClaimForUri } from 'redux/selectors/claims'; export const selectState = (state: any) => state.content || {}; export const makeSelectContentPositionForUri = (uri: string) => - createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => { - if (!claim) { - return null; + createSelector( + selectState, + makeSelectClaimForUri(uri), + (state, claim) => { + if (!claim) { + return null; + } + const outpoint = `${claim.txid}:${claim.nout}`; + const id = claim.claim_id; + return state.positions[id] ? state.positions[id][outpoint] : null; } - const outpoint = `${claim.txid}:${claim.nout}`; - const id = claim.claim_id; - return state.positions[id] ? state.positions[id][outpoint] : null; - }); + ); diff --git a/src/redux/selectors/notifications.js b/src/redux/selectors/notifications.js index 00844be..8894442 100644 --- a/src/redux/selectors/notifications.js +++ b/src/redux/selectors/notifications.js @@ -1,26 +1,32 @@ import { createSelector } from 'reselect'; -export const selectState = (state) => state.notifications || {}; +export const selectState = state => state.notifications || {}; -export const selectToast = createSelector(selectState, (state) => { - if (state.toasts.length) { - const { id, params } = state.toasts[0]; - return { - id, - ...params, - }; +export const selectToast = createSelector( + selectState, + state => { + if (state.toasts.length) { + const { id, params } = state.toasts[0]; + return { + id, + ...params, + }; + } + + return null; } +); - return null; -}); +export const selectError = createSelector( + selectState, + state => { + if (state.errors.length) { + const { error } = state.errors[0]; + return { + error, + }; + } -export const selectError = createSelector(selectState, (state) => { - if (state.errors.length) { - const { error } = state.errors[0]; - return { - error, - }; + return null; } - - return null; -}); +);