merge changes with master

This commit is contained in:
Akinwale Ariwodola 2020-02-10 12:03:20 +01:00
commit 7e2ed1f66e
26 changed files with 964 additions and 291 deletions

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2019 LBRY Inc Copyright (c) 2017-2020 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,

608
dist/bundle.es.js vendored

File diff suppressed because one or more lines are too long

View file

@ -33,6 +33,7 @@ declare type GenericClaim = {
type: 'claim' | 'update' | 'support', type: 'claim' | 'update' | 'support',
value_type: 'stream' | 'channel', value_type: 'stream' | 'channel',
signing_channel?: ChannelClaim, signing_channel?: ChannelClaim,
repost_channel_url?: string,
meta: { meta: {
activation_height: number, activation_height: number,
claims_in_channel?: number, claims_in_channel?: number,

View file

@ -1,19 +1,23 @@
declare type Comment = { declare type Comment = {
author?: string, comment: string, // comment body
author_url?: string, comment_id: string, // sha256 digest
claim_index?: number, claim_id: string, // id linking to the claim this comment
comment_id?: number, timestamp: number, // integer representing unix-time
downvotes?: number, is_hidden: boolean, // claim owner may enable/disable this
message: string, channel_id?: string, // claimId of channel signing this comment
omitted?: number, channel_name?: string, // name of channel claim
reply_count?: number, channel_url?: string, // full lbry url to signing channel
time_posted?: number, signature?: string, // signature of comment by originating channel
upvotes?: number, signing_ts?: string, // timestamp used when signing this comment
parent_id?: number, 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 = { declare type CommentsState = {
byId: {},
isLoading: boolean,
commentsByUri: { [string]: string }, commentsByUri: { [string]: string },
} byId: { [string]: Array<string> },
commentById: { [string]: Comment },
isLoading: boolean,
myComments: ?Set<string>,
};

View file

@ -125,6 +125,8 @@ declare type ChannelUpdateResponse = GenericTxResponse & {
}; };
declare type CommentCreateResponse = Comment; declare type CommentCreateResponse = Comment;
declare type CommentUpdateResponse = Comment;
declare type CommentListResponse = { declare type CommentListResponse = {
items: Array<Comment>, items: Array<Comment>,
page: number, page: number,
@ -133,6 +135,16 @@ declare type CommentListResponse = {
total_pages: number, 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 = { declare type ChannelListResponse = {
items: Array<ChannelClaim>, items: Array<ChannelClaim>,
page: number, page: number,
@ -242,6 +254,10 @@ declare type LbryTypes = {
// Commenting // Commenting
comment_list: (params: {}) => Promise<CommentListResponse>, comment_list: (params: {}) => Promise<CommentListResponse>,
comment_create: (params: {}) => Promise<CommentCreateResponse>, comment_create: (params: {}) => Promise<CommentCreateResponse>,
comment_update: (params: {}) => Promise<CommentUpdateResponse>,
comment_hide: (params: {}) => Promise<CommentHideResponse>,
comment_abandon: (params: {}) => Promise<CommentAbandonResponse>,
// Wallet utilities // Wallet utilities
wallet_balance: (params: {}) => Promise<BalanceResponse>, wallet_balance: (params: {}) => Promise<BalanceResponse>,
wallet_decrypt: (prams: {}) => Promise<boolean>, wallet_decrypt: (prams: {}) => Promise<boolean>,

1
flow-typed/Claim.js vendored
View file

@ -33,6 +33,7 @@ declare type GenericClaim = {
type: 'claim' | 'update' | 'support', type: 'claim' | 'update' | 'support',
value_type: 'stream' | 'channel', value_type: 'stream' | 'channel',
signing_channel?: ChannelClaim, signing_channel?: ChannelClaim,
repost_channel_url?: string,
meta: { meta: {
activation_height: number, activation_height: number,
claims_in_channel?: number, claims_in_channel?: number,

32
flow-typed/Comment.js vendored
View file

@ -1,19 +1,23 @@
declare type Comment = { declare type Comment = {
author?: string, comment: string, // comment body
author_url?: string, comment_id: string, // sha256 digest
claim_index?: number, claim_id: string, // id linking to the claim this comment
comment_id?: number, timestamp: number, // integer representing unix-time
downvotes?: number, is_hidden: boolean, // claim owner may enable/disable this
message: string, channel_id?: string, // claimId of channel signing this comment
omitted?: number, channel_name?: string, // name of channel claim
reply_count?: number, channel_url?: string, // full lbry url to signing channel
time_posted?: number, signature?: string, // signature of comment by originating channel
upvotes?: number, signing_ts?: string, // timestamp used when signing this comment
parent_id?: number, 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 = { declare type CommentsState = {
byId: {},
isLoading: boolean,
commentsByUri: { [string]: string }, commentsByUri: { [string]: string },
} byId: { [string]: Array<string> },
commentById: { [string]: Comment },
isLoading: boolean,
myComments: ?Set<string>,
};

16
flow-typed/Lbry.js vendored
View file

@ -125,6 +125,8 @@ declare type ChannelUpdateResponse = GenericTxResponse & {
}; };
declare type CommentCreateResponse = Comment; declare type CommentCreateResponse = Comment;
declare type CommentUpdateResponse = Comment;
declare type CommentListResponse = { declare type CommentListResponse = {
items: Array<Comment>, items: Array<Comment>,
page: number, page: number,
@ -133,6 +135,16 @@ declare type CommentListResponse = {
total_pages: number, 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 = { declare type ChannelListResponse = {
items: Array<ChannelClaim>, items: Array<ChannelClaim>,
page: number, page: number,
@ -242,6 +254,10 @@ declare type LbryTypes = {
// Commenting // Commenting
comment_list: (params: {}) => Promise<CommentListResponse>, comment_list: (params: {}) => Promise<CommentListResponse>,
comment_create: (params: {}) => Promise<CommentCreateResponse>, comment_create: (params: {}) => Promise<CommentCreateResponse>,
comment_update: (params: {}) => Promise<CommentUpdateResponse>,
comment_hide: (params: {}) => Promise<CommentHideResponse>,
comment_abandon: (params: {}) => Promise<CommentAbandonResponse>,
// Wallet utilities // Wallet utilities
wallet_balance: (params: {}) => Promise<BalanceResponse>, wallet_balance: (params: {}) => Promise<BalanceResponse>,
wallet_decrypt: (prams: {}) => Promise<boolean>, wallet_decrypt: (prams: {}) => Promise<boolean>,

View file

@ -111,6 +111,15 @@ export const COMMENT_LIST_FAILED = 'COMMENT_LIST_FAILED';
export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED'; export const COMMENT_CREATE_STARTED = 'COMMENT_CREATE_STARTED';
export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED'; export const COMMENT_CREATE_COMPLETED = 'COMMENT_CREATE_COMPLETED';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED'; 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 // Files
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED'; export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';

View file

@ -1,10 +1,10 @@
/* /*
* How to use this file: * How to use this file:
* Settings exported from here will trigger the setting to be * Settings exported from here will trigger the setting to be
* sent to the preference middleware when set using the * sent to the preference middleware when set using the
* usual setDaemonSettings and clearDaemonSettings methods. * usual setDaemonSettings and clearDaemonSettings methods.
* *
* See redux/settings/actions in the app for where this is used. * See redux/settings/actions in the app for where this is used.
*/ */
import * as DAEMON_SETTINGS from './daemon_settings'; import * as DAEMON_SETTINGS from './daemon_settings';

View file

@ -16,6 +16,8 @@ export const DEFAULT_FOLLOWED_TAGS = [
export const MATURE_TAGS = ['porn', 'nsfw', 'mature', 'xxx']; export const MATURE_TAGS = ['porn', 'nsfw', 'mature', 'xxx'];
export const DEFAULT_KNOWN_TAGS = [ export const DEFAULT_KNOWN_TAGS = [
'free speech',
'censorship',
'gaming', 'gaming',
'pop culture', 'pop culture',
'Entertainment', 'Entertainment',

View file

@ -122,7 +122,13 @@ export {
export { doToggleTagFollow, doAddTag, doDeleteTag } from 'redux/actions/tags'; 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'; export { doToggleBlockChannel } from 'redux/actions/blocked';

View file

@ -119,6 +119,11 @@ const Lbry: LbryTypes = {
// Comments // Comments
comment_list: (params = {}) => daemonCallWithResult('comment_list', params), comment_list: (params = {}) => daemonCallWithResult('comment_list', params),
comment_create: (params = {}) => daemonCallWithResult('comment_create', 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 to the sdk
connect: () => { connect: () => {
if (Lbry.connectPromise === null) { if (Lbry.connectPromise === null) {

View file

@ -43,7 +43,7 @@ export function doCommentCreate(
comment: string = '', comment: string = '',
claim_id: string = '', claim_id: string = '',
channel: ?string, channel: ?string,
parent_id?: number parent_id?: string
) { ) {
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
@ -55,11 +55,12 @@ export function doCommentCreate(
myChannels && myChannels.find(myChannel => myChannel.name === channel); myChannels && myChannels.find(myChannel => myChannel.name === channel);
const channel_id = namedChannelClaim ? namedChannelClaim.claim_id : null; const channel_id = namedChannelClaim ? namedChannelClaim.claim_id : null;
return Lbry.comment_create({ return Lbry.comment_create({
comment, comment: comment,
claim_id, claim_id: claim_id,
channel_id, channel_id: channel_id,
parent_id: parent_id,
}) })
.then((result: Comment) => { .then((result: CommentCreateResponse) => {
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_COMPLETED, type: ACTIONS.COMMENT_CREATE_COMPLETED,
data: { data: {
@ -75,10 +76,134 @@ export function doCommentCreate(
}); });
dispatch( dispatch(
doToast({ doToast({
message: 'Oops, someone broke comments.', message: 'Unable to create comment, please try again later.',
isError: true, 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,
})
);
});
};
}
}

View file

@ -1,7 +1,8 @@
// @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
export function savePosition(claimId: string, outpoint: string, position: number) { export function savePosition(claimId: string, outpoint: string, position: number) {
return dispatch => { return (dispatch: Dispatch) => {
dispatch({ dispatch({
type: ACTIONS.SET_CONTENT_POSITION, type: ACTIONS.SET_CONTENT_POSITION,
data: { claimId, outpoint, position }, data: { claimId, outpoint, position },

View file

@ -17,6 +17,15 @@ const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
type Dispatch = (action: any) => any; type Dispatch = (action: any) => any;
type GetState = () => { search: SearchState }; type GetState = () => { search: SearchState };
type SearchOptions = {
size?: number,
from?: number,
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
resolveResults?: boolean,
};
// We can't use env's because they aren't passed into node_modules // We can't use env's because they aren't passed into node_modules
let CONNECTION_STRING = 'https://lighthouse.lbry.com/'; let CONNECTION_STRING = 'https://lighthouse.lbry.com/';
@ -75,17 +84,13 @@ export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boole
} }
}; };
export const doSearch = ( export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
rawQuery: string, dispatch: Dispatch,
size: ?number, // only pass in if you don't want to use the users setting (ex: related content) getState: GetState
from: ?number, ) => {
isBackgroundSearch: boolean = false,
options: {
related_to?: string,
} = {},
resolveResults: boolean = true
) => (dispatch: Dispatch, getState: GetState) => {
const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' '); const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const resolveResults = searchOptions && searchOptions.resolveResults;
const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false;
if (!query) { if (!query) {
dispatch({ dispatch({
@ -95,9 +100,8 @@ export const doSearch = (
} }
const state = getState(); const state = getState();
let queryWithOptions = makeSelectQueryWithOptions(query, size, from, isBackgroundSearch, options)(
state let queryWithOptions = makeSelectQueryWithOptions(query, searchOptions)(state);
);
// If we have already searched for something, we don't need to do anything // If we have already searched for something, we don't need to do anything
const urisForQuery = makeSelectSearchUris(queryWithOptions)(state); const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
@ -179,15 +183,25 @@ export const doResolvedSearch = (
return; return;
} }
const optionsWithFrom: SearchOptions = {
size,
from,
isBackgroundSearch,
...options,
};
const optionsWithoutFrom: SearchOptions = {
size,
isBackgroundSearch,
...options,
};
const state = getState(); const state = getState();
let queryWithOptions = makeSelectQueryWithOptions(query, size, from, isBackgroundSearch, options)(
state let queryWithOptions = makeSelectQueryWithOptions(query, optionsWithFrom)(state);
);
// make from null so that we can maintain a reference to the same query for multiple pages and simply append the found results // make from null so that we can maintain a reference to the same query for multiple pages and simply append the found results
let queryWithoutFrom = makeSelectQueryWithOptions(query, size, null, isBackgroundSearch, options)( let queryWithoutFrom = makeSelectQueryWithOptions(query, optionsWithoutFrom)(state);
state
);
// If we have already searched for something, we don't need to do anything // If we have already searched for something, we don't need to do anything
// TODO: Tweak this check for multiple page results // TODO: Tweak this check for multiple page results
@ -245,10 +259,10 @@ export const doBlurSearchInput = () => (dispatch: Dispatch) =>
type: ACTIONS.SEARCH_BLUR, type: ACTIONS.SEARCH_BLUR,
}); });
export const doUpdateSearchOptions = (newOptions: SearchOptions) => ( export const doUpdateSearchOptions = (
dispatch: Dispatch, newOptions: SearchOptions,
getState: GetState additionalOptions: SearchOptions
) => { ) => (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const searchValue = selectSearchValue(state); const searchValue = selectSearchValue(state);
@ -259,6 +273,6 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions) => (
if (searchValue) { if (searchValue) {
// After updating, perform a search with the new options // After updating, perform a search with the new options
dispatch(doSearch(searchValue)); dispatch(doSearch(searchValue, additionalOptions));
} }
}; };

View file

@ -14,7 +14,7 @@ type SharedData = {
function extractUserState(rawObj: SharedData) { function extractUserState(rawObj: SharedData) {
if (rawObj && rawObj.version === '0.1' && rawObj.value) { if (rawObj && rawObj.version === '0.1' && rawObj.value) {
const { subscriptions, tags, blocked, settings} = rawObj.value; const { subscriptions, tags, blocked, settings } = rawObj.value;
return { return {
...(subscriptions ? { subscriptions } : {}), ...(subscriptions ? { subscriptions } : {}),
@ -30,7 +30,10 @@ function extractUserState(rawObj: SharedData) {
export function doPopulateSharedUserState(sharedSettings: any) { export function doPopulateSharedUserState(sharedSettings: any) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
const { subscriptions, tags, blocked, settings } = extractUserState(sharedSettings); 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 },
});
}; };
} }

View file

@ -33,8 +33,7 @@ export const blockedReducer = handleActions(
const { blocked } = action.data; const { blocked } = action.data;
return { return {
...state, ...state,
blockedChannels: blockedChannels: blocked && blocked.length ? blocked : state.blockedChannels,
blocked && blocked.length ? blocked : state.blockedChannels,
}; };
}, },
}, },

View file

@ -216,7 +216,9 @@ reducers[ACTIONS.FETCH_CHANNEL_LIST_COMPLETED] = (state: State, action: any): St
claims.forEach(claim => { claims.forEach(claim => {
// $FlowFixMe // $FlowFixMe
myChannelClaims.add(claim.claim_id); myChannelClaims.add(claim.claim_id);
byId[claim.claim_id] = claim; if (!byId[claim.claim_id]) {
byId[claim.claim_id] = claim;
}
if (pendingById[claim.claim_id] && claim.confirmations > 0) { if (pendingById[claim.claim_id] && claim.confirmations > 0) {
delete pendingById[claim.claim_id]; delete pendingById[claim.claim_id];
@ -265,7 +267,8 @@ reducers[ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED] = (state: State, action: any):
const paginatedClaimsByChannel = Object.assign({}, state.paginatedClaimsByChannel); const paginatedClaimsByChannel = Object.assign({}, state.paginatedClaimsByChannel);
// check if count has changed - that means cached pagination will be wrong, so clear it // check if count has changed - that means cached pagination will be wrong, so clear it
const previousCount = paginatedClaimsByChannel[uri] && paginatedClaimsByChannel[uri]['itemCount']; const previousCount = paginatedClaimsByChannel[uri] && paginatedClaimsByChannel[uri]['itemCount'];
const byChannel = (claimsInChannel === previousCount) ? Object.assign({}, paginatedClaimsByChannel[uri]) : {}; const byChannel =
claimsInChannel === previousCount ? Object.assign({}, paginatedClaimsByChannel[uri]) : {};
const allClaimIds = new Set(byChannel.all); const allClaimIds = new Set(byChannel.all);
const currentPageClaimIds = []; const currentPageClaimIds = [];
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);

View file

@ -3,9 +3,11 @@ import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
const defaultState: CommentsState = { const defaultState: CommentsState = {
byId: {}, commentById: {}, // commentId -> Comment
commentsByUri: {}, byId: {}, // ClaimID -> list of comments
commentsByUri: {}, // URI -> claimId
isLoading: false, isLoading: false,
myComments: undefined,
}; };
export const commentReducer = handleActions( export const commentReducer = handleActions(
@ -21,17 +23,24 @@ export const commentReducer = handleActions(
}), }),
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { [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 byId = Object.assign({}, state.byId);
const comments = byId[claimId]; const comments = byId[claimId];
const newComments = comments.slice(); const newCommentIds = comments.slice();
newComments.unshift(comment); // add the comment by its ID
byId[claimId] = newComments; commentById[comment.comment_id] = comment;
// push the comment_id to the top of ID list
newCommentIds.unshift(comment.comment_id);
byId[claimId] = newCommentIds;
return { return {
...state, ...state,
commentById,
byId, byId,
isLoading: false,
}; };
}, },
@ -39,16 +48,30 @@ export const commentReducer = handleActions(
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => { [ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {
const { comments, claimId, uri } = action.data; const { comments, claimId, uri } = action.data;
const commentById = Object.assign({}, state.commentById);
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
const commentsByUri = Object.assign({}, state.commentsByUri); const commentsByUri = Object.assign({}, state.commentsByUri);
if (comments) { 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; commentsByUri[uri] = claimId;
} }
return { return {
...state, ...state,
byId, byId,
commentById,
commentsByUri, commentsByUri,
isLoading: false, isLoading: false,
}; };
@ -58,6 +81,73 @@ export const commentReducer = handleActions(
...state, ...state,
isLoading: false, 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 defaultState
); );

View file

@ -114,7 +114,7 @@ export const makeSelectClaimForUri = (uri: string) =>
valid = true; valid = true;
} catch (e) {} } catch (e) {}
if (valid) { if (valid && byUri) {
const claimId = isChannel ? channelClaimId : streamClaimId; const claimId = isChannel ? channelClaimId : streamClaimId;
const pendingClaim = pendingById[claimId]; const pendingClaim = pendingById[claimId];
@ -122,7 +122,23 @@ export const makeSelectClaimForUri = (uri: string) =>
return pendingClaim; return pendingClaim;
} }
return byUri && byUri[normalizeURI(uri)]; const claim = byUri[normalizeURI(uri)];
if (claim === undefined || claim === null) {
// Make sure to return the claim as is so apps can check if it's been resolved before (null) or still needs to be resolved (undefined)
return claim;
}
const repostedClaim = claim.reposted_claim;
if (repostedClaim) {
const channelUrl = claim.signing_channel && claim.signing_channel.canonical_url;
return {
...repostedClaim,
repost_channel_url: channelUrl,
};
} else {
return claim;
}
} }
} }
); );
@ -499,7 +515,8 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
selectSearchUrisByQuery, selectSearchUrisByQuery,
(claim, searchUrisByQuery) => { makeSelectClaimIsNsfw(uri),
(claim, searchUrisByQuery, isMature) => {
const atVanityURI = !uri.includes('#'); const atVanityURI = !uri.includes('#');
let recommendedContent; let recommendedContent;
@ -513,9 +530,16 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
return; return;
} }
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), undefined, undefined, { const options: {
related_to: claim.claim_id, related_to?: string,
}); nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
if (!isMature) {
options['nsfw'] = false;
}
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
let searchUris = searchUrisByQuery[searchQuery]; let searchUris = searchUrisByQuery[searchQuery];
if (searchUris) { if (searchUris) {
@ -647,7 +671,8 @@ export const makeSelectResolvedRecommendedContentForUri = (uri: string, size: nu
createSelector( createSelector(
makeSelectClaimForUri(uri), makeSelectClaimForUri(uri),
selectResolvedSearchResultsByQuery, selectResolvedSearchResultsByQuery,
(claim, resolvedResultsByQuery) => { makeSelectClaimIsNsfw(uri),
(claim, resolvedResultsByQuery, isMature) => {
const atVanityURI = !uri.includes('#'); const atVanityURI = !uri.includes('#');
let recommendedContent; let recommendedContent;
@ -661,9 +686,16 @@ export const makeSelectResolvedRecommendedContentForUri = (uri: string, size: nu
return; return;
} }
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), { size }, undefined, { const options: {
related_to: claim.claim_id, related_to?: string,
}); nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
if (!isMature) {
options['nsfw'] = false;
}
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
let results = resolvedResultsByQuery[searchQuery]; let results = resolvedResultsByQuery[searchQuery];
if (results) { if (results) {

View file

@ -5,9 +5,35 @@ const selectState = state => state.comments || {};
export const selectCommentsById = createSelector( export const selectCommentsById = createSelector(
selectState, 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<Comments>
/* export const selectCommentsById = createSelector(
selectState,
state => state.byId || {}
); */
export const selectCommentsByUri = createSelector( export const selectCommentsByUri = createSelector(
selectState, selectState,
state => { state => {
@ -21,16 +47,20 @@ export const selectCommentsByUri = createSelector(
comments[uri] = claimId; comments[uri] = claimId;
} }
}); });
return comments; return comments;
} }
); );
export const makeSelectCommentsForUri = (uri: string) => export const makeSelectCommentsForUri = (uri: string) =>
createSelector( createSelector(
selectCommentsById, selectCommentsByClaimId,
selectCommentsByUri, selectCommentsByUri,
(byId, byUri) => { (byClaimId, byUri) => {
const claimId = byUri[uri]; 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

View file

@ -1,14 +1,19 @@
// @flow
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectClaimForUri } from 'redux/selectors/claims';
export const selectState = (state: any) => state.content || {}; export const selectState = (state: any) => state.content || {};
export const makeSelectContentPositionForUri = (uri: string) => export const makeSelectContentPositionForUri = (uri: string) =>
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => { createSelector(
if (!claim) { selectState,
return null; 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;
});

View file

@ -1,26 +1,32 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
export const selectState = (state) => state.notifications || {}; export const selectState = state => state.notifications || {};
export const selectToast = createSelector(selectState, (state) => { export const selectToast = createSelector(
if (state.toasts.length) { selectState,
const { id, params } = state.toasts[0]; state => {
return { if (state.toasts.length) {
id, const { id, params } = state.toasts[0];
...params, 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) => { return null;
if (state.errors.length) {
const { error } = state.errors[0];
return {
error,
};
} }
);
return null;
});

View file

@ -165,26 +165,27 @@ export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
// Creates a query string based on the state in the search reducer // Creates a query string based on the state in the search reducer
// Can be overrided by passing in custom sizes/from values for other areas pagination // Can be overrided by passing in custom sizes/from values for other areas pagination
type CustomOptions = {
isBackgroundSearch?: boolean,
size?: number,
from?: number,
related_to?: string,
nsfw?: boolean,
}
export const makeSelectQueryWithOptions = ( export const makeSelectQueryWithOptions = (
customQuery: ?string, customQuery: ?string,
customSize: ?number, options: CustomOptions,
customFrom: ?number,
isBackgroundSearch: boolean = false, // If it's a background search, don't use the users settings
additionalOptions: {
related_to?: string,
} = {}
) => ) =>
createSelector( createSelector(
selectSearchValue, selectSearchValue,
selectSearchOptions, selectSearchOptions,
(query, options) => { (query, defaultOptions) => {
const size = customSize || options[SEARCH_OPTIONS.RESULT_COUNT]; const searchOptions = { ...defaultOptions, ...options };
const queryString = getSearchQueryString( const queryString = getSearchQueryString(
customQuery || query, customQuery || query,
{ ...options, size, from: customFrom }, searchOptions,
!isBackgroundSearch,
additionalOptions
); );
return queryString; return queryString;

View file

@ -36,8 +36,6 @@ export function toQueryString(params: { [string]: string | number }) {
export const getSearchQueryString = ( export const getSearchQueryString = (
query: string, query: string,
options: any = {}, options: any = {},
includeUserOptions: boolean = false,
additionalOptions: {} = {}
) => { ) => {
const encodedQuery = encodeURIComponent(query); const encodedQuery = encodeURIComponent(query);
const queryParams = [ const queryParams = [
@ -45,6 +43,8 @@ export const getSearchQueryString = (
`size=${options.size || DEFAULT_SEARCH_SIZE}`, `size=${options.size || DEFAULT_SEARCH_SIZE}`,
`from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`, `from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`,
]; ];
const { isBackgroundSearch } = options;
const includeUserOptions = typeof isBackgroundSearch === 'undefined' ? false : !isBackgroundSearch;
if (includeUserOptions) { if (includeUserOptions) {
const claimType = options[SEARCH_OPTIONS.CLAIM_TYPE]; const claimType = options[SEARCH_OPTIONS.CLAIM_TYPE];
@ -68,6 +68,12 @@ export const getSearchQueryString = (
} }
} }
const additionalOptions = {}
const { related_to } = options;
const { nsfw } = options;
if (related_to) additionalOptions['related_to'] = related_to;
if (typeof nsfw !== 'undefined') additionalOptions['nsfw'] = nsfw;
if (additionalOptions) { if (additionalOptions) {
Object.keys(additionalOptions).forEach(key => { Object.keys(additionalOptions).forEach(key => {
const option = additionalOptions[key]; const option = additionalOptions[key];