0b41fc041a
* [New Feature] Comment Emotes (#125) * Refactor form-field * Create new Emote Menu * Add Emotes * Add Emote Selector and Emote Comment creation ability * Fix and Split CSS * [New Feature] Stickers (#131) * Refactor filePrice * Refactor Wallet Tip Components * Add backend sticker support for comments * Add stickers * Refactor commentCreate * Add Sticker Selector and sticker comment creation * Add stickers display to comments and hyperchats * Fix wrong checks for total Super Chats * Stickers/emojis fall out / improvements (#220) * Fix error logs * Improve LBC sticker flow/clarity * Show inline error if custom sticker amount below min * Sort emojis alphabetically * Improve loading of Images * Improve quality and display of emojis and fix CSS * Display both USD and LBC prices * Default to LBC tip if creator can't receive USD * Don't clear text-field after sticker is sent * Refactor notification component * Handle notifications * Don't show profile pic on sticker livestream comments * Change Sticker icon * Fix wording and number rounding * Fix blurring emojis * Disable non functional emote buttons * new Stickers! (#248) * Add new stickers (#347) * Fix cancel sending sticker (#447) * Refactor scrollbar CSS for portal components outside of main Refactor channelMention suggestions into new textareaSuggestions component Install @mui/material packages Move channel mentioning to use @mui/Autocomplete combobox without search functionality Add support for suggesting Emotes while typing ':' Improve label to display matching term Add back and improved support for searching while mentioning Add support for suggesting emojis Fix non concatenated strings Add key to groups and options Fix dispatch props Fix Popper positioning to be consistent Fix and Improve searching Add back support for Winning Uri Filter default emojis with the same name as emotes Remove unused topSuggestion component Fix text color on darkmode Fix livestream updating state from both websocket and reducer and causing double of the same comments to appear Fix blur and focus commentCreate events Fix no name after @ error * desktop tweaks Co-authored-by: saltrafael <76502841+saltrafael@users.noreply.github.com> Co-authored-by: Thomas Zarebczan <tzarebczan@users.noreply.github.com> Co-authored-by: Rafael <rafael.saes@odysee.com>
252 lines
9.6 KiB
JavaScript
252 lines
9.6 KiB
JavaScript
// @flow
|
|
import { getSearchQueryString } from 'util/query-params';
|
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
|
import { SEARCH_OPTIONS } from 'constants/search';
|
|
import {
|
|
selectClaimsByUri,
|
|
makeSelectClaimForUri,
|
|
makeSelectClaimForClaimId,
|
|
selectClaimIsNsfwForUri,
|
|
makeSelectPendingClaimForUri,
|
|
selectIsUriResolving,
|
|
} from 'redux/selectors/claims';
|
|
import { parseURI } from 'util/lbryURI';
|
|
import { isClaimNsfw } from 'util/claim';
|
|
import { createSelector } from 'reselect';
|
|
import { createCachedSelector } from 're-reselect';
|
|
import { createNormalizedSearchKey, getRecommendationSearchOptions } from 'util/search';
|
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
|
import { selectHistory } from 'redux/selectors/content';
|
|
import { selectAllCostInfoByUri } from 'lbryinc';
|
|
|
|
type State = { claims: any, search: SearchState };
|
|
|
|
export const selectState = (state: State): SearchState => state.search;
|
|
|
|
// $FlowFixMe - 'searchQuery' is never populated. Something lost in a merge?
|
|
export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery;
|
|
export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options;
|
|
export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching;
|
|
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = (state) =>
|
|
selectState(state).resultsByQuery;
|
|
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
|
|
selectState(state).hasReachedMaxResultsLength;
|
|
export const selectMentionSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
|
|
export const selectMentionQuery: (state: State) => string = (state) => selectState(state).mentionQuery;
|
|
|
|
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
|
createSelector(selectSearchResultByQuery, (byQuery) => {
|
|
if (!query) return;
|
|
// replace statement below is kind of ugly, and repeated in doSearch action
|
|
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
|
const normalizedQuery = createNormalizedSearchKey(query);
|
|
return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris'];
|
|
});
|
|
|
|
export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) =>
|
|
createSelector(selectHasReachedMaxResultsLength, (hasReachedMaxResultsLength) => {
|
|
if (query) {
|
|
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
|
const normalizedQuery = createNormalizedSearchKey(query);
|
|
return hasReachedMaxResultsLength[normalizedQuery];
|
|
}
|
|
return hasReachedMaxResultsLength[query];
|
|
});
|
|
|
|
export const selectRecommendedContentForUri = createCachedSelector(
|
|
(state, uri) => uri,
|
|
selectHistory,
|
|
selectClaimsByUri,
|
|
selectShowMatureContent,
|
|
selectMutedChannels,
|
|
selectAllCostInfoByUri,
|
|
selectSearchResultByQuery,
|
|
selectClaimIsNsfwForUri, // (state, uri)
|
|
(uri, history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
|
|
const claim = claimsByUri[uri];
|
|
|
|
if (!claim) return;
|
|
|
|
let recommendedContent;
|
|
// always grab the claimId - this value won't change for filtering
|
|
const currentClaimId = claim.claim_id;
|
|
|
|
const { title } = claim.value;
|
|
|
|
if (!title) return;
|
|
|
|
const options: {
|
|
size: number,
|
|
nsfw?: boolean,
|
|
isBackgroundSearch?: boolean,
|
|
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
|
|
|
|
if (matureEnabled || (!matureEnabled && !isMature)) {
|
|
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
|
|
}
|
|
|
|
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
|
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
|
|
|
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
|
|
|
if (searchResult) {
|
|
// Filter from recommended: The same claim and blocked channels
|
|
recommendedContent = searchResult['uris'].filter((searchUri) => {
|
|
const searchClaim = claimsByUri[searchUri];
|
|
|
|
if (!searchClaim) return;
|
|
|
|
const signingChannel = searchClaim && searchClaim.signing_channel;
|
|
const channelUri = signingChannel && signingChannel.canonical_url;
|
|
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
|
|
|
|
let isEqualUri;
|
|
try {
|
|
const { claimId: searchId } = parseURI(searchUri);
|
|
isEqualUri = searchId === currentClaimId;
|
|
} catch (e) {}
|
|
|
|
return !isEqualUri && !blockedMatch;
|
|
});
|
|
|
|
// Claim to play next: playable and free claims not played before in history
|
|
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
|
|
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
|
|
const recommendedClaim = claimsByUri[nextRecommendedUri];
|
|
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
|
|
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
|
|
|
|
let historyMatch = false;
|
|
try {
|
|
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
|
|
|
|
historyMatch = history.some(
|
|
(historyItem) =>
|
|
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
|
|
);
|
|
} catch (e) {}
|
|
|
|
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
|
|
})[0];
|
|
|
|
const index = recommendedContent.indexOf(nextUriToPlay);
|
|
if (index > 0) {
|
|
const a = recommendedContent[0];
|
|
recommendedContent[0] = nextUriToPlay;
|
|
recommendedContent[index] = a;
|
|
}
|
|
}
|
|
return recommendedContent;
|
|
}
|
|
)((state, uri) => String(uri));
|
|
|
|
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
|
createSelector(
|
|
makeSelectClaimForClaimId(claimId),
|
|
selectShowMatureContent,
|
|
selectSearchResultByQuery,
|
|
(claim, matureEnabled, searchUrisByQuery) => {
|
|
// TODO: DRY this out.
|
|
let poweredBy;
|
|
if (claim && claimId) {
|
|
const isMature = isClaimNsfw(claim);
|
|
const { title } = claim.value;
|
|
if (!title) {
|
|
return;
|
|
}
|
|
|
|
const options = getRecommendationSearchOptions(matureEnabled, isMature, claimId);
|
|
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
|
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
|
|
|
const searchResult = searchUrisByQuery[normalizedSearchQuery];
|
|
if (searchResult) {
|
|
poweredBy = searchResult.recsys;
|
|
} else {
|
|
return normalizedSearchQuery;
|
|
}
|
|
}
|
|
return poweredBy;
|
|
}
|
|
);
|
|
|
|
export const makeSelectWinningUriForQuery = (query: string) => {
|
|
const uriFromQuery = `lbry://${query}`;
|
|
|
|
let channelUriFromQuery = '';
|
|
try {
|
|
const { isChannel } = parseURI(uriFromQuery);
|
|
if (!isChannel) {
|
|
channelUriFromQuery = `lbry://@${query}`;
|
|
}
|
|
} catch (e) {}
|
|
|
|
return createSelector(
|
|
selectShowMatureContent,
|
|
makeSelectPendingClaimForUri(uriFromQuery),
|
|
makeSelectClaimForUri(uriFromQuery),
|
|
makeSelectClaimForUri(channelUriFromQuery),
|
|
(matureEnabled, pendingClaim, claim1, claim2) => {
|
|
const claim1Mature = claim1 && isClaimNsfw(claim1);
|
|
const claim2Mature = claim2 && isClaimNsfw(claim2);
|
|
let pendingAmount = pendingClaim && pendingClaim.amount;
|
|
|
|
if (!claim1 && !claim2) {
|
|
return undefined;
|
|
} else if (!claim1 && claim2) {
|
|
return matureEnabled ? claim2.canonical_url : claim2Mature ? undefined : claim2.canonical_url;
|
|
} else if (claim1 && !claim2) {
|
|
return matureEnabled
|
|
? claim1.repost_url || claim1.canonical_url
|
|
: claim1Mature
|
|
? undefined
|
|
: claim1.repost_url || claim1.canonical_url;
|
|
}
|
|
|
|
const effectiveAmount1 = claim1 && (claim1.repost_bid_amount || claim1.meta.effective_amount);
|
|
// claim2 will never have a repost_bid_amount because reposts never start with "@"
|
|
const effectiveAmount2 = claim2 && claim2.meta.effective_amount;
|
|
|
|
if (!matureEnabled) {
|
|
if (claim1Mature && !claim2Mature) {
|
|
return claim2.canonical_url;
|
|
} else if (claim2Mature && !claim1Mature) {
|
|
return claim1.repost_url || claim1.canonical_url;
|
|
} else if (claim1Mature && claim2Mature) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const returnBeforePending =
|
|
Number(effectiveAmount1) > Number(effectiveAmount2)
|
|
? claim1.repost_url || claim1.canonical_url
|
|
: claim2.canonical_url;
|
|
if (pendingAmount && pendingAmount > effectiveAmount1 && pendingAmount > effectiveAmount2) {
|
|
return pendingAmount.permanent_url;
|
|
} else {
|
|
return returnBeforePending;
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
export const selectIsResolvingWinningUri = (state: State, query: string = '') => {
|
|
const uriFromQuery = `lbry://${query}`;
|
|
let channelUriFromQuery;
|
|
try {
|
|
const { isChannel } = parseURI(uriFromQuery);
|
|
if (!isChannel) {
|
|
channelUriFromQuery = `lbry://@${query}`;
|
|
}
|
|
} catch (e) {}
|
|
|
|
const claim1IsResolving = selectIsUriResolving(state, uriFromQuery);
|
|
const claim2IsResolving = channelUriFromQuery ? selectIsUriResolving(state, channelUriFromQuery) : false;
|
|
return claim1IsResolving || claim2IsResolving;
|
|
};
|
|
|
|
export const makeSelectUrlForClaimId = (claimId: string) =>
|
|
createSelector(makeSelectClaimForClaimId(claimId), (claim) =>
|
|
claim ? claim.canonical_url || claim.permanent_url : null
|
|
);
|