diff --git a/LICENSE b/LICENSE index 200acb5..e8be4ab 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ 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 "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, diff --git a/dist/bundle.es.js b/dist/bundle.es.js index 1c85e26..c83f84b 100644 --- a/dist/bundle.es.js +++ b/dist/bundle.es.js @@ -134,6 +134,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'; @@ -380,6 +389,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, @@ -771,12 +789,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; @@ -809,7 +827,7 @@ const DEFAULT_FOLLOWED_TAGS = ['art', 'automotive', 'blockchain', 'comedy', 'eco const MATURE_TAGS = ['porn', 'nsfw', 'mature', 'xxx']; -const DEFAULT_KNOWN_TAGS = ['gaming', 'pop culture', 'Entertainment', 'technology', 'music', 'funny', 'Education', 'learning', 'news', 'gameplay', 'nature', 'beliefs', 'comedy', 'games', 'sony interactive entertainment', 'film & animation', 'game', 'weapons', "let's play", 'blockchain', 'video game', 'sports', 'walkthrough', 'ps4live', 'art', 'pc', 'minecraft', 'playthrough', 'economics', 'automotive', 'play', 'ps4share', 'tutorial', 'twitch', 'how to', 'ps4', 'bitcoin', 'fortnite', 'commentary', 'lets play', 'fun', 'politics', 'xbox', 'autos & vehicles', 'Travel & Events', 'food', 'science', 'xbox one', 'liberal', 'democrat', 'progressive', 'survival', 'Nonprofits & Activism', 'cryptocurrency', 'playstation', 'nintendo', 'government', 'steam', 'podcast', 'gamer', 'horror', 'conservative', 'reaction', 'trailer', 'love', 'cnn', 'republican', 'political', 'hangoutsonair', 'hoa', 'msnbc', 'cbs', 'anime', 'donald trump', 'fiction', 'fox news', 'crypto', 'ethereum', 'call of duty', 'android', 'multiplayer', 'epic', 'rpg', 'adventure', 'secular talk', 'btc', 'atheist', 'atheism', 'video games', 'ps3', 'cod', 'online', 'agnostic', 'movie', 'fps', 'lets', 'mod', 'world', 'reviews', 'sharefactory', 'space', 'pokemon', 'stream', 'hilarious', 'lol', 'sony', 'god', "let's", 'dance', 'pvp', 'tech', 'strategy', 'zombies', 'fail', 'film', 'xbox 360', 'animation', 'unboxing', 'money', 'how', 'travel', 'wwe', 'mods', 'indie', 'pubg', 'ios', 'history', 'rap', 'sony computer entertainment', 'mobile', 'trump', 'hack', 'flat earth', 'trap', 'humor', 'vlogging', 'fox', 'news radio', 'facebook', 'edm', 'fitness', 'vaping', 'hip hop', 'secular', 'jesus', 'song', 'vape', 'guitar', 'remix', 'mining', 'daily', 'diy', 'pets & animals', 'videogame', 'death', 'funny moments', 'religion', 'media', 'viral', 'war', 'nbc', 'freedom', 'gold', 'family', 'meme', 'zombie', 'photography', 'chill', 'sniper', 'computer', 'iphone', 'dragon', 'bible', 'pro', 'overwatch', 'litecoin', 'gta', 'house', 'fire', 'bass', 'bitcoin news', 'truth', 'crash', 'mario', 'league of legends', 'wii', 'mmorpg', 'grand theft auto v', 'health', 'marvel', 'racing', 'apple', 'instrumental', 'earth', 'destiny', 'satire', 'race', 'training', 'electronic', 'boss', 'roblox', 'family friendly', 'california', 'react', 'christian', 'mmo', 'twitter', 'help', 'star', 'cars', 'random', 'top 10', 'ninja', 'guns', 'linux', 'lessons', 'vegan', 'future', 'dota 2', 'studio', 'star wars', 'gta 5', 'shooting', 'nasa', 'rock', 'league', 'subscribe', 'water', 'gta v', 'car', 'samsung', 'music video', 'skyrim', 'dog', 'comics', 'shooter game', 'bo3', 'halloween', 'liberty', 'eth', 'conspiracy', 'knife', 'fashion', 'stories', 'vapor', 'nvidia', 'cute', 'beat', 'nintendo switch', 'fantasy', 'christmas', 'world of warcraft', 'industry', 'cartoon', 'crypto news', 'garden', 'animals', 'windows', 'happy', 'magic', 'memes', 'design', 'tactical', 'fallout 4', 'puzzle', 'parody', 'rv', 'beats', 'fortnite battle royale', 'building', 'disney', 'drone', 'ps2', 'beach', 'metal', 'christianity', 'business', 'mix', 'bo2', 'cover', 'senate', '4k', 'united states', 'final', 'hero', 'playing', 'dlc', 'ubisoft', 'halo', 'pc gaming', 'raw', 'investing', 'online learning', 'software', 'ark', 'mojang', 'console', 'battle royale', 'canon', 'microsoft', 'camping', 'cryptocurrency news', 'ufo', 'progressive talk', 'switch', 'fpv', 'arcade', 'school', 'driving', 'bodybuilding', 'drama', 'retro', 'science fiction', 'eggs', 'australia', 'modded', 'rainbow', 'gamers', 'resident evil', 'drawing', 'brasil', 'england', 'hillary clinton', 'singing', 'final fantasy', 'hiphop', 'video blog', 'mature', 'quad', 'noob', 'simulation', 'illuminati', 'poetry', 'dayz', 'manga', 'howto', 'insane', 'xbox360', 'press', 'special', 'church', 'ico', 'weird', 'libertarian', 'crafting', 'level', 'comic', 'sandbox', 'daily vlog', 'outdoor', 'black ops', 'sound', 'christ', 'duty', 'Juvenile fiction', 'pc game', 'how-to', 'ww2', 'creepy', 'artist', 'galaxy', 'destiny 2', 'new music', 'quest', 'lee', 'pacman', 'super smash bros', 'day', 'survival horror', 'patreon', 'bitcoin price', 'trending', 'open world', 'wii u', 'dope', 'reaper', 'sniping', 'dubstep', 'truck', 'planet', 'dc', 'amazon', 'spirituality', 'universe', 'video game culture', 'community', 'cat', 'aliens', 'tourism', 'altcoins', 'style', 'travel trailer', 'rda', '5859dfec-026f-46ba-bea0-02bf43aa1a6f', 'gun', 'secret', 'far cry 5', 'auto', 'culture', 'dj', 'mw2', 'lord', 'full time rving', 'role-playing game', 'prank', 'grand theft auto', 'master', 'wrestling', 'sci-fi', 'workout', 'ghost', 'fake news', 'silly', 'season', 'bo4', 'trading', 'extreme', 'economy', 'combat', 'plays', 'muslim', 'pubg mobile', 'clips', 'bo1', 'paypal', 'sims', 'exploration', 'light', 'ripple', 'paranormal', 'football', 'capcom', 'rta', 'discord', 'action role-playing game', 'playthrough part', 'batman', 'player', 'server', 'anarchy', 'military', 'playlist', 'cosplay', 'rv park', 'rant', 'edit', 'germany', 'reading', 'chris', 'flash', 'loot', 'bitcoin gratis', 'game reviews', 'movies', 'stupid', 'latest news', 'squad gameplay', 'guru', 'timelapse', 'black ops 3', 'holiday', 'soul', 'motivation', 'mw3', 'vacation', 'sega', '19th century', 'pop', 'sims 4', 'post', 'smok', 'island', 'scotland', 'paladins', 'warrior', 'creepypasta', 'role-playing video game', 'solar', 'vr', 'animal', 'peace', 'consciousness', 'dota', 'audio', 'mass effect', 'Humour', 'first look', 'videogames', 'future bass', 'freestyle', 'hardcore', 'portugal', 'dantdm', 'teaser', 'lbry']; +const DEFAULT_KNOWN_TAGS = ['free speech', 'censorship', 'gaming', 'pop culture', 'Entertainment', 'technology', 'music', 'funny', 'Education', 'learning', 'news', 'gameplay', 'nature', 'beliefs', 'comedy', 'games', 'sony interactive entertainment', 'film & animation', 'game', 'weapons', "let's play", 'blockchain', 'video game', 'sports', 'walkthrough', 'ps4live', 'art', 'pc', 'minecraft', 'playthrough', 'economics', 'automotive', 'play', 'ps4share', 'tutorial', 'twitch', 'how to', 'ps4', 'bitcoin', 'fortnite', 'commentary', 'lets play', 'fun', 'politics', 'xbox', 'autos & vehicles', 'Travel & Events', 'food', 'science', 'xbox one', 'liberal', 'democrat', 'progressive', 'survival', 'Nonprofits & Activism', 'cryptocurrency', 'playstation', 'nintendo', 'government', 'steam', 'podcast', 'gamer', 'horror', 'conservative', 'reaction', 'trailer', 'love', 'cnn', 'republican', 'political', 'hangoutsonair', 'hoa', 'msnbc', 'cbs', 'anime', 'donald trump', 'fiction', 'fox news', 'crypto', 'ethereum', 'call of duty', 'android', 'multiplayer', 'epic', 'rpg', 'adventure', 'secular talk', 'btc', 'atheist', 'atheism', 'video games', 'ps3', 'cod', 'online', 'agnostic', 'movie', 'fps', 'lets', 'mod', 'world', 'reviews', 'sharefactory', 'space', 'pokemon', 'stream', 'hilarious', 'lol', 'sony', 'god', "let's", 'dance', 'pvp', 'tech', 'strategy', 'zombies', 'fail', 'film', 'xbox 360', 'animation', 'unboxing', 'money', 'how', 'travel', 'wwe', 'mods', 'indie', 'pubg', 'ios', 'history', 'rap', 'sony computer entertainment', 'mobile', 'trump', 'hack', 'flat earth', 'trap', 'humor', 'vlogging', 'fox', 'news radio', 'facebook', 'edm', 'fitness', 'vaping', 'hip hop', 'secular', 'jesus', 'song', 'vape', 'guitar', 'remix', 'mining', 'daily', 'diy', 'pets & animals', 'videogame', 'death', 'funny moments', 'religion', 'media', 'viral', 'war', 'nbc', 'freedom', 'gold', 'family', 'meme', 'zombie', 'photography', 'chill', 'sniper', 'computer', 'iphone', 'dragon', 'bible', 'pro', 'overwatch', 'litecoin', 'gta', 'house', 'fire', 'bass', 'bitcoin news', 'truth', 'crash', 'mario', 'league of legends', 'wii', 'mmorpg', 'grand theft auto v', 'health', 'marvel', 'racing', 'apple', 'instrumental', 'earth', 'destiny', 'satire', 'race', 'training', 'electronic', 'boss', 'roblox', 'family friendly', 'california', 'react', 'christian', 'mmo', 'twitter', 'help', 'star', 'cars', 'random', 'top 10', 'ninja', 'guns', 'linux', 'lessons', 'vegan', 'future', 'dota 2', 'studio', 'star wars', 'gta 5', 'shooting', 'nasa', 'rock', 'league', 'subscribe', 'water', 'gta v', 'car', 'samsung', 'music video', 'skyrim', 'dog', 'comics', 'shooter game', 'bo3', 'halloween', 'liberty', 'eth', 'conspiracy', 'knife', 'fashion', 'stories', 'vapor', 'nvidia', 'cute', 'beat', 'nintendo switch', 'fantasy', 'christmas', 'world of warcraft', 'industry', 'cartoon', 'crypto news', 'garden', 'animals', 'windows', 'happy', 'magic', 'memes', 'design', 'tactical', 'fallout 4', 'puzzle', 'parody', 'rv', 'beats', 'fortnite battle royale', 'building', 'disney', 'drone', 'ps2', 'beach', 'metal', 'christianity', 'business', 'mix', 'bo2', 'cover', 'senate', '4k', 'united states', 'final', 'hero', 'playing', 'dlc', 'ubisoft', 'halo', 'pc gaming', 'raw', 'investing', 'online learning', 'software', 'ark', 'mojang', 'console', 'battle royale', 'canon', 'microsoft', 'camping', 'cryptocurrency news', 'ufo', 'progressive talk', 'switch', 'fpv', 'arcade', 'school', 'driving', 'bodybuilding', 'drama', 'retro', 'science fiction', 'eggs', 'australia', 'modded', 'rainbow', 'gamers', 'resident evil', 'drawing', 'brasil', 'england', 'hillary clinton', 'singing', 'final fantasy', 'hiphop', 'video blog', 'mature', 'quad', 'noob', 'simulation', 'illuminati', 'poetry', 'dayz', 'manga', 'howto', 'insane', 'xbox360', 'press', 'special', 'church', 'ico', 'weird', 'libertarian', 'crafting', 'level', 'comic', 'sandbox', 'daily vlog', 'outdoor', 'black ops', 'sound', 'christ', 'duty', 'Juvenile fiction', 'pc game', 'how-to', 'ww2', 'creepy', 'artist', 'galaxy', 'destiny 2', 'new music', 'quest', 'lee', 'pacman', 'super smash bros', 'day', 'survival horror', 'patreon', 'bitcoin price', 'trending', 'open world', 'wii u', 'dope', 'reaper', 'sniping', 'dubstep', 'truck', 'planet', 'dc', 'amazon', 'spirituality', 'universe', 'video game culture', 'community', 'cat', 'aliens', 'tourism', 'altcoins', 'style', 'travel trailer', 'rda', '5859dfec-026f-46ba-bea0-02bf43aa1a6f', 'gun', 'secret', 'far cry 5', 'auto', 'culture', 'dj', 'mw2', 'lord', 'full time rving', 'role-playing game', 'prank', 'grand theft auto', 'master', 'wrestling', 'sci-fi', 'workout', 'ghost', 'fake news', 'silly', 'season', 'bo4', 'trading', 'extreme', 'economy', 'combat', 'plays', 'muslim', 'pubg mobile', 'clips', 'bo1', 'paypal', 'sims', 'exploration', 'light', 'ripple', 'paranormal', 'football', 'capcom', 'rta', 'discord', 'action role-playing game', 'playthrough part', 'batman', 'player', 'server', 'anarchy', 'military', 'playlist', 'cosplay', 'rv park', 'rant', 'edit', 'germany', 'reading', 'chris', 'flash', 'loot', 'bitcoin gratis', 'game reviews', 'movies', 'stupid', 'latest news', 'squad gameplay', 'guru', 'timelapse', 'black ops 3', 'holiday', 'soul', 'motivation', 'mw3', 'vacation', 'sega', '19th century', 'pop', 'sims 4', 'post', 'smok', 'island', 'scotland', 'paladins', 'warrior', 'creepypasta', 'role-playing video game', 'solar', 'vr', 'animal', 'peace', 'consciousness', 'dota', 'audio', 'mass effect', 'Humour', 'first look', 'videogames', 'future bass', 'freestyle', 'hardcore', 'portugal', 'dantdm', 'teaser', 'lbry']; // @@ -922,6 +940,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) { @@ -1048,9 +1071,11 @@ function toQueryString(params) { return parts.join('&'); } -const getSearchQueryString = (query, options = {}, includeUserOptions = false, additionalOptions = {}) => { +const getSearchQueryString = (query, options = {}) => { const encodedQuery = encodeURIComponent(query); const queryParams = [`s=${encodedQuery}`, `size=${options.size || DEFAULT_SEARCH_SIZE}`, `from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`]; + const { isBackgroundSearch } = options; + const includeUserOptions = typeof isBackgroundSearch === 'undefined' ? false : !isBackgroundSearch; if (includeUserOptions) { const claimType = options[SEARCH_OPTIONS.CLAIM_TYPE]; @@ -1062,6 +1087,12 @@ const getSearchQueryString = (query, options = {}, includeUserOptions = false, a } } + 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) { Object.keys(additionalOptions).forEach(key => { const option = additionalOptions[key]; @@ -1420,11 +1451,11 @@ const selectSearchSuggestions = reselect.createSelector(selectSearchValue, selec // 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 -const makeSelectQueryWithOptions = (customQuery, customSize, customFrom, isBackgroundSearch = false, // If it's a background search, don't use the users settings -additionalOptions = {}) => reselect.createSelector(selectSearchValue, selectSearchOptions, (query, options) => { - const size = customSize || options[SEARCH_OPTIONS.RESULT_COUNT]; - const queryString = getSearchQueryString(customQuery || query, _extends$1({}, options, { size, from: customFrom }), !isBackgroundSearch, additionalOptions); + +const makeSelectQueryWithOptions = (customQuery, options) => reselect.createSelector(selectSearchValue, selectSearchOptions, (query, defaultOptions) => { + const searchOptions = _extends$1({}, defaultOptions, options); + const queryString = getSearchQueryString(customQuery || query, searchOptions); return queryString; }); @@ -1553,7 +1584,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 } + }); }; } @@ -1908,7 +1942,7 @@ function concatClaims(claimList = [], concatClaimList = []) { return claims; } -// +var _extends$4 = 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$2 = state => state.claims || {}; const selectClaimsById = reselect.createSelector(selectState$2, state => state.byId || {}); @@ -1977,7 +2011,7 @@ const makeSelectClaimForUri = uri => reselect.createSelector(selectClaimsByUri, valid = true; } catch (e) {} - if (valid) { + if (valid && byUri) { const claimId = isChannel ? channelClaimId : streamClaimId; const pendingClaim = pendingById[claimId]; @@ -1985,7 +2019,22 @@ const makeSelectClaimForUri = uri => reselect.createSelector(selectClaimsByUri, 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 _extends$4({}, repostedClaim, { + repost_channel_url: channelUrl + }); + } else { + return claim; + } } }); @@ -2194,7 +2243,7 @@ claim => { return isClaimNsfw(claim); }); -const makeSelectRecommendedContentForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), selectSearchUrisByQuery, (claim, searchUrisByQuery) => { +const makeSelectRecommendedContentForUri = uri => reselect.createSelector(makeSelectClaimForUri(uri), selectSearchUrisByQuery, makeSelectClaimIsNsfw(uri), (claim, searchUrisByQuery, isMature) => { const atVanityURI = !uri.includes('#'); let recommendedContent; @@ -2208,9 +2257,12 @@ const makeSelectRecommendedContentForUri = uri => reselect.createSelector(makeSe return; } - const searchQuery = getSearchQueryString(title.replace(/\//, ' '), undefined, undefined, { - related_to: claim.claim_id - }); + const options = { related_to: claim.claim_id, isBackgroundSearch: true }; + + if (!isMature) { + options['nsfw'] = false; + } + const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options); let searchUris = searchUrisByQuery[searchQuery]; if (searchUris) { @@ -2283,7 +2335,7 @@ const makeSelectMyStreamUrlsForPage = (page = 1) => reselect.createSelector(sele const selectMyStreamUrlsCount = reselect.createSelector(selectMyClaimUrisWithoutChannels, channels => channels.length); -const makeSelectResolvedRecommendedContentForUri = (uri, size) => reselect.createSelector(makeSelectClaimForUri(uri), selectResolvedSearchResultsByQuery, (claim, resolvedResultsByQuery) => { +const makeSelectResolvedRecommendedContentForUri = (uri, size) => reselect.createSelector(makeSelectClaimForUri(uri), selectResolvedSearchResultsByQuery, makeSelectClaimIsNsfw(uri), (claim, resolvedResultsByQuery, isMature) => { const atVanityURI = !uri.includes('#'); let recommendedContent; @@ -2297,9 +2349,12 @@ const makeSelectResolvedRecommendedContentForUri = (uri, size) => reselect.creat return; } - const searchQuery = getSearchQueryString(title.replace(/\//, ' '), { size }, undefined, { - related_to: claim.claim_id - }); + const options = { related_to: claim.claim_id, isBackgroundSearch: true }; + if (!isMature) { + options['nsfw'] = false; + } + + const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options); let results = resolvedResultsByQuery[searchQuery]; if (results) { @@ -2741,7 +2796,7 @@ function batchActions(...actions) { }; } -var _extends$4 = 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$5 = 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 doResolveUris(uris, returnCachedClaims = false) { return (dispatch, getState) => { @@ -2781,7 +2836,7 @@ function doResolveUris(uris, returnCachedClaims = false) { // https://github.com/facebook/flow/issues/2221 if (uriResolveInfo) { if (uriResolveInfo.error) { - resolveInfo[uri] = _extends$4({}, fallbackResolveInfo); + resolveInfo[uri] = _extends$5({}, fallbackResolveInfo); } else { let result = {}; if (uriResolveInfo.value_type === 'channel') { @@ -3471,7 +3526,7 @@ function doSetFileListSort(page, value) { }; } -var _extends$5 = 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$6 = 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$2(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; } @@ -3514,7 +3569,7 @@ const selectPublishFormValues = reselect.createSelector(selectState$5, selectIsS } else { actualLanguage = language || 'en'; } - return _extends$5({}, formValues, { language: actualLanguage }); + return _extends$6({}, formValues, { language: actualLanguage }); }); const makeSelectPublishFormValue = item => reselect.createSelector(selectState$5, state => state[item]); @@ -3569,7 +3624,7 @@ const selectTakeOverAmount = reselect.createSelector(selectState$5, selectMyClai return null; }); -var _extends$6 = 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$7 = 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 doResetThumbnailStatus = () => dispatch => { dispatch({ @@ -3607,7 +3662,7 @@ const doClearPublish = () => dispatch => { const doUpdatePublishForm = publishFormValue => dispatch => dispatch({ type: UPDATE_PUBLISH_FORM, - data: _extends$6({}, publishFormValue) + data: _extends$7({}, publishFormValue) }); const doUploadThumbnail = (filePath, thumbnailBlob, fsAdapter, fs, path) => dispatch => { @@ -3925,7 +3980,7 @@ function handleFetchResponse(response) { return response.status === 200 ? Promise.resolve(response.json()) : Promise.reject(new Error(response.statusText)); } -// +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 DEBOUNCED_SEARCH_SUGGESTION_MS = 300; @@ -3982,9 +4037,10 @@ const doUpdateSearchQuery = (query, shouldSkipSuggestions) => dispatch => { } }; -const doSearch = (rawQuery, size, // only pass in if you don't want to use the users setting (ex: related content) -from, isBackgroundSearch = false, options = {}, resolveResults = true) => (dispatch, getState) => { +const doSearch = (rawQuery, searchOptions) => (dispatch, getState) => { const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' '); + const resolveResults = searchOptions && searchOptions.resolveResults; + const isBackgroundSearch = searchOptions && searchOptions.isBackgroundSearch || false; if (!query) { dispatch({ @@ -3994,7 +4050,8 @@ from, isBackgroundSearch = false, options = {}, resolveResults = true) => (dispa } 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 const urisForQuery = makeSelectSearchUris(queryWithOptions)(state); @@ -4065,11 +4122,23 @@ from, isBackgroundSearch = false, options = {}, nsfw) => (dispatch, getState) => return; } + const optionsWithFrom = _extends$8({ + size, + from, + isBackgroundSearch + }, options); + + const optionsWithoutFrom = _extends$8({ + size, + isBackgroundSearch + }, options); + 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 - let queryWithoutFrom = makeSelectQueryWithOptions(query, size, null, isBackgroundSearch, options)(state); + let queryWithoutFrom = makeSelectQueryWithOptions(query, optionsWithoutFrom)(state); // If we have already searched for something, we don't need to do anything // TODO: Tweak this check for multiple page results @@ -4120,7 +4189,7 @@ const doBlurSearchInput = () => dispatch => dispatch({ type: SEARCH_BLUR }); -const doUpdateSearchOptions = newOptions => (dispatch, getState) => { +const doUpdateSearchOptions = (newOptions, additionalOptions) => (dispatch, getState) => { const state = getState(); const searchValue = selectSearchValue(state); @@ -4131,10 +4200,12 @@ const doUpdateSearchOptions = newOptions => (dispatch, getState) => { if (searchValue) { // After updating, perform a search with the new options - dispatch(doSearch(searchValue)); + dispatch(doSearch(searchValue, additionalOptions)); } }; +// + function savePosition(claimId, outpoint, position) { return dispatch => { dispatch({ @@ -4212,9 +4283,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, @@ -4229,13 +4301,121 @@ function doCommentCreate(comment = '', claim_id = '', channel, parent_id) { data: error }); dispatch(doToast({ - message: 'Oops, someone broke comments.', + 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 => ({ @@ -4245,7 +4425,7 @@ const doToggleBlockChannel = uri => ({ } }); -var _extends$7 = 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$9 = 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 = {}; const defaultState = { @@ -4348,7 +4528,7 @@ reducers[RESOLVE_URIS_STARTED] = (state, action) => { }; reducers[RESOLVE_URIS_COMPLETED] = (state, action) => { - return _extends$7({}, handleClaimAction(state, action)); + return _extends$9({}, handleClaimAction(state, action)); }; reducers[FETCH_CLAIM_LIST_MINE_STARTED] = state => Object.assign({}, state, { @@ -4411,7 +4591,9 @@ reducers[FETCH_CHANNEL_LIST_COMPLETED] = (state, action) => { claims.forEach(claim => { // $FlowFixMe 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) { delete pendingById[claim.claim_id]; @@ -4519,7 +4701,7 @@ reducers[ABANDON_CLAIM_SUCCEEDED] = (state, action) => { }); }; -reducers[CREATE_CHANNEL_STARTED] = state => _extends$7({}, state, { +reducers[CREATE_CHANNEL_STARTED] = state => _extends$9({}, state, { creatingChannel: true, createChannelError: null }); @@ -4607,7 +4789,7 @@ reducers[CLAIM_SEARCH_COMPLETED] = (state, action) => { delete fetchingClaimSearchByQuery[query]; - return Object.assign({}, state, _extends$7({}, handleClaimAction(state, action), { + return Object.assign({}, state, _extends$9({}, handleClaimAction(state, action), { claimSearchByQuery, claimSearchByQueryLastPageReached, fetchingClaimSearchByQuery @@ -4647,61 +4829,142 @@ const handleActions = (actionMap, defaultState) => (state = defaultState, action return state; }; -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; }; +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; }; 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({ - [COMMENT_CREATE_STARTED]: (state, action) => _extends$8({}, state, { + [COMMENT_CREATE_STARTED]: (state, action) => _extends$a({}, state, { isLoading: true }), - [COMMENT_CREATE_FAILED]: (state, action) => _extends$8({}, state, { + [COMMENT_CREATE_FAILED]: (state, action) => _extends$a({}, 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 newComments = comments.slice(); + const newCommentIds = comments.slice(); - newComments.unshift(comment); - byId[claimId] = newComments; + // add the comment by its ID + commentById[comment.comment_id] = comment; - return _extends$8({}, state, { - byId + // push the comment_id to the top of ID list + newCommentIds.unshift(comment.comment_id); + byId[claimId] = newCommentIds; + + return _extends$a({}, state, { + commentById, + byId, + isLoading: false }); }, - [COMMENT_LIST_STARTED]: state => _extends$8({}, state, { isLoading: true }), + [COMMENT_LIST_STARTED]: state => _extends$a({}, 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) { - 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, { + return _extends$a({}, state, { byId, + commentById, commentsByUri, isLoading: false }); }, - [COMMENT_LIST_FAILED]: (state, action) => _extends$8({}, state, { + [COMMENT_LIST_FAILED]: (state, action) => _extends$a({}, state, { + isLoading: false + }), + [COMMENT_ABANDON_STARTED]: (state, action) => _extends$a({}, 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$a({}, state, { + commentById, + byId, + isLoading: false + }); + }, + // do nothing + [COMMENT_ABANDON_FAILED]: (state, action) => _extends$a({}, state, { + isLoading: false + }), + // do nothing + [COMMENT_UPDATE_STARTED]: (state, action) => _extends$a({}, 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$a({}, state, { + commentById, + isLoading: false + }); + }, + // nothing can be done here + [COMMENT_UPDATE_FAILED]: (state, action) => _extends$a({}, state, { + isLoading: false + }), + // nothing can really be done here + [COMMENT_HIDE_STARTED]: (state, action) => _extends$a({}, state, { + isLoading: true + }), + [COMMENT_HIDE_COMPLETED]: (state, action) => _extends$a({}, state, { // todo: add HiddenComments state & create selectors + isLoading: false + }), + // nothing can be done here + [COMMENT_HIDE_FAILED]: (state, action) => _extends$a({}, state, { isLoading: false }) }, defaultState$1); -var _extends$9 = 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$b = 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 = { @@ -4710,9 +4973,9 @@ const defaultState$2 = { reducers$1[SET_CONTENT_POSITION] = (state, action) => { const { claimId, outpoint, position } = action.data; - return _extends$9({}, state, { - positions: _extends$9({}, state.positions, { - [claimId]: _extends$9({}, state.positions[claimId], { + return _extends$b({}, state, { + positions: _extends$b({}, state.positions, { + [claimId]: _extends$b({}, state.positions[claimId], { [outpoint]: position }) }) @@ -4879,7 +5142,7 @@ function fileInfoReducer(state = defaultState$3, action) { return state; } -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; }; +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$3 = {}; const defaultState$4 = { @@ -4895,7 +5158,7 @@ reducers$3[PURCHASE_URI_STARTED] = (state, action) => { newFailedPurchaseUris.splice(newFailedPurchaseUris.indexOf(uri), 1); } - return _extends$a({}, state, { + return _extends$c({}, state, { failedPurchaseUris: newFailedPurchaseUris, purchaseUriErrorMessage: '' }); @@ -4913,7 +5176,7 @@ reducers$3[PURCHASE_URI_COMPLETED] = (state, action) => { newFailedPurchaseUris.splice(newFailedPurchaseUris.indexOf(uri), 1); } - return _extends$a({}, state, { + return _extends$c({}, state, { failedPurchaseUris: newFailedPurchaseUris, purchasedUris: newPurchasedUris, purchaseUriErrorMessage: '' @@ -4928,7 +5191,7 @@ reducers$3[PURCHASE_URI_FAILED] = (state, action) => { newFailedPurchaseUris.push(uri); } - return _extends$a({}, state, { + return _extends$c({}, state, { failedPurchaseUris: newFailedPurchaseUris, purchaseUriErrorMessage: error }); @@ -4941,7 +5204,7 @@ reducers$3[DELETE_PURCHASED_URI] = (state, action) => { newPurchasedUris.splice(newPurchasedUris.indexOf(uri), 1); } - return _extends$a({}, state, { + return _extends$c({}, state, { purchasedUris: newPurchasedUris }); }; @@ -4952,7 +5215,7 @@ function fileReducer(state = defaultState$4, action) { return state; } -var _extends$b = 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$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$5 = { notifications: [], @@ -4967,7 +5230,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.push(toast); - return _extends$b({}, state, { + return _extends$d({}, state, { toasts: newToasts }); }, @@ -4975,7 +5238,7 @@ const notificationsReducer = handleActions({ const newToasts = state.toasts.slice(); newToasts.shift(); - return _extends$b({}, state, { + return _extends$d({}, state, { toasts: newToasts }); }, @@ -4986,7 +5249,7 @@ const notificationsReducer = handleActions({ const newNotifications = state.notifications.slice(); newNotifications.push(notification); - return _extends$b({}, state, { + return _extends$d({}, state, { notifications: newNotifications }); }, @@ -4997,7 +5260,7 @@ const notificationsReducer = handleActions({ notifications = notifications.map(pastNotification => pastNotification.id === notification.id ? notification : pastNotification); - return _extends$b({}, state, { + return _extends$d({}, state, { notifications }); }, @@ -5006,7 +5269,7 @@ const notificationsReducer = handleActions({ let newNotifications = state.notifications.slice(); newNotifications = newNotifications.filter(notification => notification.id !== id); - return _extends$b({}, state, { + return _extends$d({}, state, { notifications: newNotifications }); }, @@ -5017,7 +5280,7 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.push(error); - return _extends$b({}, state, { + return _extends$d({}, state, { errors: newErrors }); }, @@ -5025,13 +5288,13 @@ const notificationsReducer = handleActions({ const newErrors = state.errors.slice(); newErrors.shift(); - return _extends$b({}, state, { + return _extends$d({}, state, { errors: newErrors }); } }, defaultState$5); -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; }; +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; }; function _objectWithoutProperties$3(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; } @@ -5068,17 +5331,17 @@ const defaultState$6 = { const publishReducer = handleActions({ [UPDATE_PUBLISH_FORM]: (state, action) => { const { data } = action; - return _extends$c({}, state, data); + return _extends$e({}, state, data); }, - [CLEAR_PUBLISH]: () => _extends$c({}, defaultState$6), - [PUBLISH_START]: state => _extends$c({}, state, { + [CLEAR_PUBLISH]: () => _extends$e({}, defaultState$6), + [PUBLISH_START]: state => _extends$e({}, state, { publishing: true, publishSuccess: false }), - [PUBLISH_FAIL]: state => _extends$c({}, state, { + [PUBLISH_FAIL]: state => _extends$e({}, state, { publishing: false }), - [PUBLISH_SUCCESS]: state => _extends$c({}, state, { + [PUBLISH_SUCCESS]: state => _extends$e({}, state, { publishing: false, publishSuccess: true }), @@ -5093,14 +5356,14 @@ const publishReducer = handleActions({ streamName: name }); - return _extends$c({}, defaultState$6, publishData, { + return _extends$e({}, defaultState$6, publishData, { editingURI: uri, uri: shortUri }); } }, defaultState$6); -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; }; +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; }; const defaultState$7 = { isActive: false, // does the user have any typed text in the search input @@ -5122,23 +5385,23 @@ const defaultState$7 = { }; const searchReducer = handleActions({ - [SEARCH_START]: state => _extends$d({}, state, { + [SEARCH_START]: state => _extends$f({}, state, { searching: true }), [SEARCH_SUCCESS]: (state, action) => { const { query, uris } = action.data; - return _extends$d({}, state, { + return _extends$f({}, state, { searching: false, urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }) }); }, - [SEARCH_FAIL]: state => _extends$d({}, state, { + [SEARCH_FAIL]: state => _extends$f({}, state, { searching: false }), - [RESOLVED_SEARCH_START]: state => _extends$d({}, state, { + [RESOLVED_SEARCH_START]: state => _extends$f({}, state, { searching: true }), [RESOLVED_SEARCH_SUCCESS]: (state, action) => { @@ -5156,24 +5419,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$d({}, state, { + return _extends$f({}, state, { searching: false, resolvedResultsByQuery, resolvedResultsByQueryLastPageReached }); }, - [RESOLVED_SEARCH_FAIL]: state => _extends$d({}, state, { + [RESOLVED_SEARCH_FAIL]: state => _extends$f({}, state, { searching: false }), - [UPDATE_SEARCH_QUERY]: (state, action) => _extends$d({}, state, { + [UPDATE_SEARCH_QUERY]: (state, action) => _extends$f({}, state, { searchQuery: action.data.query, isActive: true }), - [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$d({}, state, { - suggestions: _extends$d({}, state.suggestions, { + [UPDATE_SEARCH_SUGGESTIONS]: (state, action) => _extends$f({}, state, { + suggestions: _extends$f({}, state.suggestions, { [action.data.query]: action.data.suggestions }) }), @@ -5181,30 +5444,30 @@ 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$d({}, state, { + [DISMISS_NOTIFICATION]: state => _extends$f({}, state, { isActive: false }), - [SEARCH_FOCUS]: state => _extends$d({}, state, { + [SEARCH_FOCUS]: state => _extends$f({}, state, { focused: true }), - [SEARCH_BLUR]: state => _extends$d({}, state, { + [SEARCH_BLUR]: state => _extends$f({}, state, { focused: false }), [UPDATE_SEARCH_OPTIONS]: (state, action) => { const { options: oldOptions } = state; const newOptions = action.data; - const options = _extends$d({}, oldOptions, newOptions); - return _extends$d({}, state, { + const options = _extends$f({}, oldOptions, newOptions); + return _extends$f({}, state, { options }); } }, defaultState$7); -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$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; }; function getDefaultKnownTags() { - return DEFAULT_FOLLOWED_TAGS.concat(DEFAULT_KNOWN_TAGS).reduce((tagsMap, tag) => _extends$e({}, tagsMap, { + return DEFAULT_FOLLOWED_TAGS.concat(DEFAULT_KNOWN_TAGS).reduce((tagsMap, tag) => _extends$g({}, tagsMap, { [tag]: { name: tag } }), {}); } @@ -5227,7 +5490,7 @@ const tagsReducer = handleActions({ newFollowedTags.push(name); } - return _extends$e({}, state, { + return _extends$g({}, state, { followedTags: newFollowedTags }); }, @@ -5236,10 +5499,10 @@ const tagsReducer = handleActions({ const { knownTags } = state; const { name } = action.data; - let newKnownTags = _extends$e({}, knownTags); + let newKnownTags = _extends$g({}, knownTags); newKnownTags[name] = { name }; - return _extends$e({}, state, { + return _extends$g({}, state, { knownTags: newKnownTags }); }, @@ -5248,24 +5511,24 @@ const tagsReducer = handleActions({ const { knownTags, followedTags } = state; const { name } = action.data; - let newKnownTags = _extends$e({}, knownTags); + let newKnownTags = _extends$g({}, knownTags); delete newKnownTags[name]; const newFollowedTags = followedTags.filter(tag => tag !== name); - return _extends$e({}, state, { + return _extends$g({}, state, { knownTags: newKnownTags, followedTags: newFollowedTags }); }, [USER_STATE_POPULATE]: (state, action) => { const { tags } = action.data; - return _extends$e({}, state, { + return _extends$g({}, state, { followedTags: tags && tags.length ? tags : DEFAULT_FOLLOWED_TAGS }); } }, defaultState$8); -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$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; }; const defaultState$9 = { blockedChannels: [] @@ -5289,13 +5552,13 @@ const blockedReducer = handleActions({ }, [USER_STATE_POPULATE]: (state, action) => { const { blocked } = action.data; - return _extends$f({}, state, { + return _extends$h({}, state, { blockedChannels: blocked && blocked.length ? blocked : state.blockedChannels }); } }, defaultState$9); -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$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 buildDraftTransaction = () => ({ amount: undefined, @@ -5340,25 +5603,25 @@ const defaultState$a = { }; const walletReducer = handleActions({ - [FETCH_TRANSACTIONS_STARTED]: state => _extends$g({}, state, { + [FETCH_TRANSACTIONS_STARTED]: state => _extends$i({}, state, { fetchingTransactions: true }), [FETCH_TRANSACTIONS_COMPLETED]: (state, action) => { - const byId = _extends$g({}, state.transactions); + const byId = _extends$i({}, state.transactions); const { transactions } = action.data; transactions.forEach(transaction => { byId[transaction.txid] = transaction; }); - return _extends$g({}, state, { + return _extends$i({}, state, { transactions: byId, fetchingTransactions: false }); }, - [FETCH_SUPPORTS_STARTED]: state => _extends$g({}, state, { + [FETCH_SUPPORTS_STARTED]: state => _extends$i({}, state, { fetchingSupports: true }), @@ -5371,7 +5634,7 @@ const walletReducer = handleActions({ byOutpoint[`${txid}:${nout}`] = transaction; }); - return _extends$g({}, state, { supports: byOutpoint, fetchingSupports: false }); + return _extends$i({}, state, { supports: byOutpoint, fetchingSupports: false }); }, [ABANDON_SUPPORT_STARTED]: (state, action) => { @@ -5380,7 +5643,7 @@ const walletReducer = handleActions({ currentlyAbandoning[outpoint] = true; - return _extends$g({}, state, { + return _extends$i({}, state, { abandoningSupportsByOutpoint: currentlyAbandoning }); }, @@ -5393,23 +5656,23 @@ const walletReducer = handleActions({ delete currentlyAbandoning[outpoint]; delete byOutpoint[outpoint]; - return _extends$g({}, state, { + return _extends$i({}, state, { supports: byOutpoint, abandoningSupportsById: currentlyAbandoning }); }, - [GET_NEW_ADDRESS_STARTED]: state => _extends$g({}, state, { + [GET_NEW_ADDRESS_STARTED]: state => _extends$i({}, state, { gettingNewAddress: true }), [GET_NEW_ADDRESS_COMPLETED]: (state, action) => { const { address } = action.data; - return _extends$g({}, state, { gettingNewAddress: false, receiveAddress: address }); + return _extends$i({}, state, { gettingNewAddress: false, receiveAddress: address }); }, - [UPDATE_BALANCE]: (state, action) => _extends$g({}, state, { + [UPDATE_BALANCE]: (state, action) => _extends$i({}, state, { totalBalance: action.data.totalBalance, balance: action.data.balance, reservedBalance: action.data.reservedBalance, @@ -5418,32 +5681,32 @@ const walletReducer = handleActions({ tipsBalance: action.data.tipsBalance }), - [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$g({}, state, { + [CHECK_ADDRESS_IS_MINE_STARTED]: state => _extends$i({}, state, { checkingAddressOwnership: true }), - [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$g({}, state, { + [CHECK_ADDRESS_IS_MINE_COMPLETED]: state => _extends$i({}, state, { checkingAddressOwnership: false }), [SET_DRAFT_TRANSACTION_AMOUNT]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$g({}, oldDraft, { amount: parseFloat(action.data.amount) }); + const newDraft = _extends$i({}, oldDraft, { amount: parseFloat(action.data.amount) }); - return _extends$g({}, state, { draftTransaction: newDraft }); + return _extends$i({}, state, { draftTransaction: newDraft }); }, [SET_DRAFT_TRANSACTION_ADDRESS]: (state, action) => { const oldDraft = state.draftTransaction; - const newDraft = _extends$g({}, oldDraft, { address: action.data.address }); + const newDraft = _extends$i({}, oldDraft, { address: action.data.address }); - return _extends$g({}, state, { draftTransaction: newDraft }); + return _extends$i({}, state, { draftTransaction: newDraft }); }, [SEND_TRANSACTION_STARTED]: state => { - const newDraftTransaction = _extends$g({}, state.draftTransaction, { sending: true }); + const newDraftTransaction = _extends$i({}, state.draftTransaction, { sending: true }); - return _extends$g({}, state, { draftTransaction: newDraftTransaction }); + return _extends$i({}, state, { draftTransaction: newDraftTransaction }); }, [SEND_TRANSACTION_COMPLETED]: state => Object.assign({}, state, { @@ -5456,118 +5719,120 @@ const walletReducer = handleActions({ error: action.data.error }); - return _extends$g({}, state, { draftTransaction: newDraftTransaction }); + return _extends$i({}, state, { draftTransaction: newDraftTransaction }); }, - [SUPPORT_TRANSACTION_STARTED]: state => _extends$g({}, state, { + [SUPPORT_TRANSACTION_STARTED]: state => _extends$i({}, state, { sendingSupport: true }), - [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$g({}, state, { + [SUPPORT_TRANSACTION_COMPLETED]: state => _extends$i({}, state, { sendingSupport: false }), - [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$g({}, state, { + [SUPPORT_TRANSACTION_FAILED]: (state, action) => _extends$i({}, state, { error: action.data.error, sendingSupport: false }), - [CLEAR_SUPPORT_TRANSACTION]: state => _extends$g({}, state, { + [CLEAR_SUPPORT_TRANSACTION]: state => _extends$i({}, state, { sendingSupport: false }), - [WALLET_STATUS_COMPLETED]: (state, action) => _extends$g({}, state, { + [WALLET_STATUS_COMPLETED]: (state, action) => _extends$i({}, state, { walletIsEncrypted: action.result }), - [WALLET_ENCRYPT_START]: state => _extends$g({}, state, { + [WALLET_ENCRYPT_START]: state => _extends$i({}, state, { walletEncryptPending: true, walletEncryptSucceded: null, walletEncryptResult: null }), - [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$g({}, state, { + [WALLET_ENCRYPT_COMPLETED]: (state, action) => _extends$i({}, state, { walletEncryptPending: false, walletEncryptSucceded: true, walletEncryptResult: action.result }), - [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$g({}, state, { + [WALLET_ENCRYPT_FAILED]: (state, action) => _extends$i({}, state, { walletEncryptPending: false, walletEncryptSucceded: false, walletEncryptResult: action.result }), - [WALLET_DECRYPT_START]: state => _extends$g({}, state, { + [WALLET_DECRYPT_START]: state => _extends$i({}, state, { walletDecryptPending: true, walletDecryptSucceded: null, walletDecryptResult: null }), - [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$g({}, state, { + [WALLET_DECRYPT_COMPLETED]: (state, action) => _extends$i({}, state, { walletDecryptPending: false, walletDecryptSucceded: true, walletDecryptResult: action.result }), - [WALLET_DECRYPT_FAILED]: (state, action) => _extends$g({}, state, { + [WALLET_DECRYPT_FAILED]: (state, action) => _extends$i({}, state, { walletDecryptPending: false, walletDecryptSucceded: false, walletDecryptResult: action.result }), - [WALLET_UNLOCK_START]: state => _extends$g({}, state, { + [WALLET_UNLOCK_START]: state => _extends$i({}, state, { walletUnlockPending: true, walletUnlockSucceded: null, walletUnlockResult: null }), - [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$g({}, state, { + [WALLET_UNLOCK_COMPLETED]: (state, action) => _extends$i({}, state, { walletUnlockPending: false, walletUnlockSucceded: true, walletUnlockResult: action.result }), - [WALLET_UNLOCK_FAILED]: (state, action) => _extends$g({}, state, { + [WALLET_UNLOCK_FAILED]: (state, action) => _extends$i({}, state, { walletUnlockPending: false, walletUnlockSucceded: false, walletUnlockResult: action.result }), - [WALLET_LOCK_START]: state => _extends$g({}, state, { + [WALLET_LOCK_START]: state => _extends$i({}, state, { walletLockPending: false, walletLockSucceded: null, walletLockResult: null }), - [WALLET_LOCK_COMPLETED]: (state, action) => _extends$g({}, state, { + [WALLET_LOCK_COMPLETED]: (state, action) => _extends$i({}, state, { walletLockPending: false, walletLockSucceded: true, walletLockResult: action.result }), - [WALLET_LOCK_FAILED]: (state, action) => _extends$g({}, state, { + [WALLET_LOCK_FAILED]: (state, action) => _extends$i({}, state, { walletLockPending: false, walletLockSucceded: false, walletLockResult: action.result }), - [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$g({}, state, { + [SET_TRANSACTION_LIST_FILTER]: (state, action) => _extends$i({}, state, { transactionListFilter: action.data }), - [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$g({}, state, { + [UPDATE_CURRENT_HEIGHT]: (state, action) => _extends$i({}, state, { latestBlock: action.data }), - [WALLET_RESTART]: state => _extends$g({}, state, { + [WALLET_RESTART]: state => _extends$i({}, state, { walletReconnecting: true }), - [WALLET_RESTART_COMPLETED]: state => _extends$g({}, state, { + [WALLET_RESTART_COMPLETED]: state => _extends$i({}, state, { walletReconnecting: false }) }, defaultState$a); +// + const selectState$6 = state => state.content || {}; const makeSelectContentPositionForUri = uri => reselect.createSelector(selectState$6, makeSelectClaimForUri(uri), (state, claim) => { @@ -5579,14 +5844,14 @@ const makeSelectContentPositionForUri = uri => reselect.createSelector(selectSta return state.positions[id] ? state.positions[id][outpoint] : null; }); -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$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 selectState$7 = state => state.notifications || {}; const selectToast = reselect.createSelector(selectState$7, state => { if (state.toasts.length) { const { id, params } = state.toasts[0]; - return _extends$h({ + return _extends$j({ id }, params); } @@ -5609,8 +5874,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 = {}; @@ -5622,14 +5909,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 || {}; @@ -5707,8 +5998,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/Claim.js b/dist/flow-typed/Claim.js index 54e414f..5ebe5e9 100644 --- a/dist/flow-typed/Claim.js +++ b/dist/flow-typed/Claim.js @@ -33,6 +33,7 @@ declare type GenericClaim = { type: 'claim' | 'update' | 'support', value_type: 'stream' | 'channel', signing_channel?: ChannelClaim, + repost_channel_url?: string, meta: { activation_height: number, claims_in_channel?: number, 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/Claim.js b/flow-typed/Claim.js index 54e414f..5ebe5e9 100644 --- a/flow-typed/Claim.js +++ b/flow-typed/Claim.js @@ -33,6 +33,7 @@ declare type GenericClaim = { type: 'claim' | 'update' | 'support', value_type: 'stream' | 'channel', signing_channel?: ChannelClaim, + repost_channel_url?: string, meta: { activation_height: number, claims_in_channel?: number, 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/constants/tags.js b/src/constants/tags.js index e44fe37..3a1e26d 100644 --- a/src/constants/tags.js +++ b/src/constants/tags.js @@ -16,6 +16,8 @@ export const DEFAULT_FOLLOWED_TAGS = [ export const MATURE_TAGS = ['porn', 'nsfw', 'mature', 'xxx']; export const DEFAULT_KNOWN_TAGS = [ + 'free speech', + 'censorship', 'gaming', 'pop culture', 'Entertainment', 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..0bcb8cb 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: { @@ -75,10 +76,134 @@ export function doCommentCreate( }); dispatch( doToast({ - message: 'Oops, someone broke comments.', + 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/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/search.js b/src/redux/actions/search.js index fc17b4b..6094277 100644 --- a/src/redux/actions/search.js +++ b/src/redux/actions/search.js @@ -17,6 +17,15 @@ const DEBOUNCED_SEARCH_SUGGESTION_MS = 300; type Dispatch = (action: any) => any; 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 let CONNECTION_STRING = 'https://lighthouse.lbry.com/'; @@ -75,17 +84,13 @@ export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boole } }; -export const doSearch = ( - rawQuery: string, - size: ?number, // only pass in if you don't want to use the users setting (ex: related content) - from: ?number, - isBackgroundSearch: boolean = false, - options: { - related_to?: string, - } = {}, - resolveResults: boolean = true -) => (dispatch: Dispatch, getState: GetState) => { +export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( + dispatch: Dispatch, + getState: GetState +) => { const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' '); + const resolveResults = searchOptions && searchOptions.resolveResults; + const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false; if (!query) { dispatch({ @@ -95,9 +100,8 @@ export const doSearch = ( } 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 const urisForQuery = makeSelectSearchUris(queryWithOptions)(state); @@ -179,15 +183,25 @@ export const doResolvedSearch = ( return; } + const optionsWithFrom: SearchOptions = { + size, + from, + isBackgroundSearch, + ...options, + }; + + const optionsWithoutFrom: SearchOptions = { + size, + isBackgroundSearch, + ...options, + }; + 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 - let queryWithoutFrom = makeSelectQueryWithOptions(query, size, null, isBackgroundSearch, options)( - state - ); + let queryWithoutFrom = makeSelectQueryWithOptions(query, optionsWithoutFrom)(state); // If we have already searched for something, we don't need to do anything // TODO: Tweak this check for multiple page results @@ -245,10 +259,10 @@ export const doBlurSearchInput = () => (dispatch: Dispatch) => type: ACTIONS.SEARCH_BLUR, }); -export const doUpdateSearchOptions = (newOptions: SearchOptions) => ( - dispatch: Dispatch, - getState: GetState -) => { +export const doUpdateSearchOptions = ( + newOptions: SearchOptions, + additionalOptions: SearchOptions +) => (dispatch: Dispatch, getState: GetState) => { const state = getState(); const searchValue = selectSearchValue(state); @@ -259,6 +273,6 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions) => ( if (searchValue) { // After updating, perform a search with the new options - dispatch(doSearch(searchValue)); + dispatch(doSearch(searchValue, additionalOptions)); } }; 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/claims.js b/src/redux/reducers/claims.js index 8e6b121..f60cdca 100644 --- a/src/redux/reducers/claims.js +++ b/src/redux/reducers/claims.js @@ -216,7 +216,9 @@ reducers[ACTIONS.FETCH_CHANNEL_LIST_COMPLETED] = (state: State, action: any): St claims.forEach(claim => { // $FlowFixMe 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) { 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); // check if count has changed - that means cached pagination will be wrong, so clear it 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 currentPageClaimIds = []; const byId = Object.assign({}, state.byId); diff --git a/src/redux/reducers/comments.js b/src/redux/reducers/comments.js index 3d1738b..46d08d8 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,73 @@ 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); + 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/claims.js b/src/redux/selectors/claims.js index 6adf368..73a3018 100644 --- a/src/redux/selectors/claims.js +++ b/src/redux/selectors/claims.js @@ -114,7 +114,7 @@ export const makeSelectClaimForUri = (uri: string) => valid = true; } catch (e) {} - if (valid) { + if (valid && byUri) { const claimId = isChannel ? channelClaimId : streamClaimId; const pendingClaim = pendingById[claimId]; @@ -122,7 +122,23 @@ export const makeSelectClaimForUri = (uri: string) => 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( makeSelectClaimForUri(uri), selectSearchUrisByQuery, - (claim, searchUrisByQuery) => { + makeSelectClaimIsNsfw(uri), + (claim, searchUrisByQuery, isMature) => { const atVanityURI = !uri.includes('#'); let recommendedContent; @@ -513,9 +530,16 @@ export const makeSelectRecommendedContentForUri = (uri: string) => return; } - const searchQuery = getSearchQueryString(title.replace(/\//, ' '), undefined, undefined, { - related_to: claim.claim_id, - }); + const options: { + 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]; if (searchUris) { @@ -647,7 +671,8 @@ export const makeSelectResolvedRecommendedContentForUri = (uri: string, size: nu createSelector( makeSelectClaimForUri(uri), selectResolvedSearchResultsByQuery, - (claim, resolvedResultsByQuery) => { + makeSelectClaimIsNsfw(uri), + (claim, resolvedResultsByQuery, isMature) => { const atVanityURI = !uri.includes('#'); let recommendedContent; @@ -661,9 +686,16 @@ export const makeSelectResolvedRecommendedContentForUri = (uri: string, size: nu return; } - const searchQuery = getSearchQueryString(title.replace(/\//, ' '), { size }, undefined, { - related_to: claim.claim_id, - }); + const options: { + 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]; if (results) { 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; -}); +); diff --git a/src/redux/selectors/search.js b/src/redux/selectors/search.js index 48dca04..e3efd11 100644 --- a/src/redux/selectors/search.js +++ b/src/redux/selectors/search.js @@ -165,26 +165,27 @@ export const selectSearchSuggestions: Array = createSelector( // 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 + +type CustomOptions = { + isBackgroundSearch?: boolean, + size?: number, + from?: number, + related_to?: string, + nsfw?: boolean, +} + export const makeSelectQueryWithOptions = ( customQuery: ?string, - customSize: ?number, - customFrom: ?number, - isBackgroundSearch: boolean = false, // If it's a background search, don't use the users settings - additionalOptions: { - related_to?: string, - } = {} + options: CustomOptions, ) => createSelector( selectSearchValue, selectSearchOptions, - (query, options) => { - const size = customSize || options[SEARCH_OPTIONS.RESULT_COUNT]; - + (query, defaultOptions) => { + const searchOptions = { ...defaultOptions, ...options }; const queryString = getSearchQueryString( customQuery || query, - { ...options, size, from: customFrom }, - !isBackgroundSearch, - additionalOptions + searchOptions, ); return queryString; diff --git a/src/util/query-params.js b/src/util/query-params.js index 7a3da29..80a1d54 100644 --- a/src/util/query-params.js +++ b/src/util/query-params.js @@ -36,8 +36,6 @@ export function toQueryString(params: { [string]: string | number }) { export const getSearchQueryString = ( query: string, options: any = {}, - includeUserOptions: boolean = false, - additionalOptions: {} = {} ) => { const encodedQuery = encodeURIComponent(query); const queryParams = [ @@ -45,6 +43,8 @@ export const getSearchQueryString = ( `size=${options.size || DEFAULT_SEARCH_SIZE}`, `from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`, ]; + const { isBackgroundSearch } = options; + const includeUserOptions = typeof isBackgroundSearch === 'undefined' ? false : !isBackgroundSearch; if (includeUserOptions) { 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) { Object.keys(additionalOptions).forEach(key => { const option = additionalOptions[key];