-
Notifications
You must be signed in to change notification settings - Fork 414
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
commit bb3c979 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 13:57:33 2021 -0300 Use canonical url commit 18bcaed Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:05:26 2021 -0300 Fix name display and appeareance commit 413dad6 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 13:17:25 2021 -0300 Handle punctuation after mention commit e7b92d4 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:03:38 2021 -0300 Fix breaking for invalid URI query commit d4615cb Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:03:14 2021 -0300 Fix mentioning with enter on livestream commit 6be2fb9 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:02:43 2021 -0300 Improve logic for locating a mention commit 26ae3a4 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:02:07 2021 -0300 Fix mentioned user name being smaller than other text commit ff0b526 Author: saltrafael <a657ee92-7eb9-46eb-97d2-6735799df838@aleeas.com> Date: Tue Sep 28 11:01:49 2021 -0300 Add Channel Mention selection ability commit ddc29ac Author: saltrafael <76502841+saltrafael@users.noreply.github.com> Date: Mon Sep 27 16:10:41 2021 -0300 Fix autoplay next default value (#7173) commit 3e8172a Author: Thomas Zarebczan <tzarebczan@users.noreply.github.com> Date: Mon Sep 27 12:40:06 2021 -0400 force mp3 extension vs mpga
- Loading branch information
saltrafael
committed
Sep 28, 2021
1 parent
799d71d
commit cbcb529
Showing
20 changed files
with
705 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { connect } from 'react-redux'; | ||
import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux'; | ||
import ChannelMentionSuggestion from './view'; | ||
|
||
const select = (state, props) => ({ | ||
claim: makeSelectClaimForUri(props.uri)(state), | ||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), | ||
}); | ||
|
||
export default connect(select)(ChannelMentionSuggestion); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// @flow | ||
import { ComboboxOption } from '@reach/combobox'; | ||
import ChannelThumbnail from 'component/channelThumbnail'; | ||
import React from 'react'; | ||
|
||
type Props = { | ||
claim: ?Claim, | ||
uri?: string, | ||
isResolvingUri: boolean, | ||
}; | ||
|
||
export default function ChannelMentionSuggestion(props: Props) { | ||
const { claim, uri, isResolvingUri } = props; | ||
|
||
return !claim ? null : ( | ||
<ComboboxOption value={uri}> | ||
{isResolvingUri ? ( | ||
<div className="channel-mention__suggestion"> | ||
<div className="media__thumb media__thumb--resolving" /> | ||
</div> | ||
) : ( | ||
<div className="channel-mention__suggestion"> | ||
<ChannelThumbnail xsmall uri={uri} /> | ||
<span className="channel-mention__suggestion-label"> | ||
<div className="channel-mention__suggestion-title">{(claim.value && claim.value.title) || claim.name}</div> | ||
<div className="channel-mention__suggestion-name">{claim.name}</div> | ||
</span> | ||
</div> | ||
)} | ||
</ComboboxOption> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { connect } from 'react-redux'; | ||
import { selectShowMatureContent } from 'redux/selectors/settings'; | ||
import { selectSubscriptions } from 'redux/selectors/subscriptions'; | ||
import { withRouter } from 'react-router'; | ||
import { doResolveUris, makeSelectClaimForUri } from 'lbry-redux'; | ||
import { makeSelectTopLevelCommentsForUri } from 'redux/selectors/comments'; | ||
import ChannelMentionSuggestions from './view'; | ||
|
||
const select = (state, props) => { | ||
const subscriptionUris = selectSubscriptions(state).map(({ uri }) => uri); | ||
const topLevelComments = makeSelectTopLevelCommentsForUri(props.uri)(state); | ||
|
||
const commentorUris = []; | ||
topLevelComments.map(({ channel_url }) => !commentorUris.includes(channel_url) && commentorUris.push(channel_url)); | ||
|
||
const getUnresolved = (uris) => | ||
uris.map((uri) => !makeSelectClaimForUri(uri)(state) && uri).filter((uri) => uri !== false); | ||
const getCanonical = (uris) => | ||
uris | ||
.map((uri) => makeSelectClaimForUri(uri)(state) && makeSelectClaimForUri(uri)(state).canonical_url) | ||
.filter((uri) => Boolean(uri)); | ||
|
||
return { | ||
commentorUris, | ||
subscriptionUris, | ||
unresolvedCommentors: getUnresolved(commentorUris), | ||
unresolvedSubscriptions: getUnresolved(subscriptionUris), | ||
canonicalCreator: getCanonical([props.creatorUri]), | ||
canonicalCommentors: getCanonical(commentorUris), | ||
canonicalSubscriptions: getCanonical(subscriptionUris), | ||
showMature: selectShowMatureContent(state), | ||
}; | ||
}; | ||
|
||
export default withRouter(connect(select, { doResolveUris })(ChannelMentionSuggestions)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
// @flow | ||
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox'; | ||
import { Form } from 'component/common/form'; | ||
import { parseURI, regexInvalidURI } from 'lbry-redux'; | ||
import { SEARCH_OPTIONS } from 'constants/search'; | ||
import * as KEYCODES from 'constants/keycodes'; | ||
import ChannelMentionSuggestion from 'component/channelMentionSuggestion'; | ||
import ChannelMentionTopSuggestion from 'component/channelMentionTopSuggestion'; | ||
import React from 'react'; | ||
import Spinner from 'component/spinner'; | ||
import type { ElementRef } from 'react'; | ||
import useLighthouse from 'effects/use-lighthouse'; | ||
|
||
const INPUT_DEBOUNCE_MS = 1000; | ||
const LIGHTHOUSE_MIN_CHARACTERS = 3; | ||
|
||
type Props = { | ||
inputRef: any, | ||
mentionTerm: string, | ||
noTopSuggestion?: boolean, | ||
showMature: boolean, | ||
creatorUri: string, | ||
isLivestream: boolean, | ||
commentorUris: Array<string>, | ||
unresolvedCommentors: Array<string>, | ||
subscriptionUris: Array<string>, | ||
unresolvedSubscriptions: Array<string>, | ||
canonicalCreator: Array<string>, | ||
canonicalCommentors: Array<string>, | ||
canonicalSubscriptions: Array<string>, | ||
doResolveUris: (Array<string>) => void, | ||
isInputFocused: boolean, | ||
customSelectAction?: (string, number) => void, | ||
}; | ||
|
||
export default function ChannelMentionSuggestions(props: Props) { | ||
const { | ||
unresolvedCommentors, | ||
unresolvedSubscriptions, | ||
canonicalCreator, | ||
canonicalCommentors, | ||
canonicalSubscriptions, | ||
isLivestream, | ||
creatorUri, | ||
inputRef, | ||
showMature, | ||
noTopSuggestion, | ||
mentionTerm, | ||
doResolveUris, | ||
isInputFocused, | ||
customSelectAction, | ||
} = props; | ||
const comboboxInputRef: ElementRef<any> = React.useRef(); | ||
const comboboxListRef: ElementRef<any> = React.useRef(); | ||
const [debouncedTerm, setDebouncedTerm] = React.useState(''); | ||
const mainEl = document.querySelector('.channel-mention__suggestions'); | ||
const [isFocused, setIsFocused] = React.useState(false); | ||
const [canonicalResults, setCanonicalResults] = React.useState([]); | ||
|
||
const isRefFocused = (ref) => ref && ref.current === document.activeElement; | ||
|
||
const subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri); | ||
const commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri)); | ||
|
||
const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase(); | ||
const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris]; | ||
const allShownCanonical = [ | ||
canonicalCreator[0], | ||
...canonicalSubscriptions, | ||
...canonicalCommentors, | ||
...canonicalResults, | ||
]; | ||
const possibleMatches = allShownUris.filter((uri) => { | ||
try { | ||
const { channelName } = parseURI(uri); | ||
return channelName.toLowerCase().includes(termToMatch); | ||
} catch (e) {} | ||
}); | ||
const hasSubscriptionsResolved = | ||
subscriptionUris && | ||
!subscriptionUris.every((uri) => unresolvedSubscriptions && unresolvedSubscriptions.includes(uri)); | ||
|
||
const searchSize = 5; | ||
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS }; | ||
const { results, loading } = useLighthouse(debouncedTerm, showMature, searchSize, additionalOptions, 0); | ||
const stringifiedResults = JSON.stringify(results); | ||
|
||
const hasMinLength = mentionTerm && mentionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS; | ||
const isTyping = debouncedTerm !== mentionTerm; | ||
const showPlaceholder = isTyping || loading; | ||
|
||
const isUriFromTermValid = !regexInvalidURI.test(mentionTerm.substring(1)); | ||
|
||
const handleSelect = React.useCallback( | ||
(value, key) => { | ||
if (customSelectAction) { | ||
// Give them full results, as our resolved one might truncate the claimId. | ||
customSelectAction(value || (results && results.find((r) => r.startsWith(value))) || '', Number(key)); | ||
} | ||
}, | ||
[customSelectAction, results] | ||
); | ||
|
||
React.useEffect(() => { | ||
const timer = setTimeout(() => { | ||
if (isTyping) setDebouncedTerm(!hasMinLength ? '' : mentionTerm); | ||
}, INPUT_DEBOUNCE_MS); | ||
|
||
return () => clearTimeout(timer); | ||
}, [isTyping, mentionTerm, hasMinLength, possibleMatches.length]); | ||
|
||
React.useEffect(() => { | ||
if (!mainEl) return; | ||
const header = document.querySelector('.header__navigation'); | ||
|
||
function handleReflow() { | ||
const boxAtTopOfPage = header && mainEl.getBoundingClientRect().top <= header.offsetHeight; | ||
const boxAtBottomOfPage = mainEl.getBoundingClientRect().bottom >= window.innerHeight; | ||
|
||
if (boxAtTopOfPage) { | ||
mainEl.setAttribute('flow-bottom', ''); | ||
} | ||
if (mainEl.getAttribute('flow-bottom') !== null && boxAtBottomOfPage) { | ||
mainEl.removeAttribute('flow-bottom'); | ||
} | ||
} | ||
handleReflow(); | ||
|
||
window.addEventListener('scroll', handleReflow); | ||
return () => window.removeEventListener('scroll', handleReflow); | ||
}, [mainEl]); | ||
|
||
React.useEffect(() => { | ||
if (!inputRef || !comboboxInputRef || !mentionTerm) return; | ||
|
||
function handleKeyDown(event) { | ||
const { keyCode } = event; | ||
const activeElement = document.activeElement; | ||
setIsFocused(isRefFocused(comboboxInputRef) || isRefFocused(inputRef)); | ||
|
||
if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) { | ||
if (isRefFocused(comboboxInputRef)) { | ||
const selectedId = activeElement && activeElement.getAttribute('aria-activedescendant'); | ||
const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`); | ||
if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' }); | ||
} else { | ||
// $FlowFixMe | ||
comboboxInputRef.current.focus(); | ||
} | ||
} else { | ||
if ((isRefFocused(comboboxInputRef) || isRefFocused(inputRef)) && keyCode === KEYCODES.TAB) { | ||
event.preventDefault(); | ||
const activeValue = activeElement && activeElement.getAttribute('value'); | ||
|
||
if (activeValue) { | ||
handleSelect(activeValue, keyCode); | ||
} else if (possibleMatches.length) { | ||
const suggest = allShownCanonical.find((matchUri) => possibleMatches.find((uri) => uri.includes(matchUri))); | ||
if (suggest) handleSelect(suggest, keyCode); | ||
} else if (results) { | ||
handleSelect(mentionTerm, keyCode); | ||
} | ||
} | ||
if (isRefFocused(comboboxInputRef)) { | ||
// $FlowFixMe | ||
inputRef.current.focus(); | ||
} | ||
} | ||
} | ||
|
||
window.addEventListener('keydown', handleKeyDown); | ||
|
||
return () => window.removeEventListener('keydown', handleKeyDown); | ||
}, [allShownCanonical, handleSelect, inputRef, mentionTerm, possibleMatches, results]); | ||
|
||
React.useEffect(() => { | ||
if (!stringifiedResults) return; | ||
|
||
const arrayResults = JSON.parse(stringifiedResults); | ||
if (arrayResults && arrayResults.length > 0) { | ||
// $FlowFixMe | ||
doResolveUris(arrayResults).then((response) => { | ||
try { | ||
// $FlowFixMe | ||
const canonical_urls = Object.values(response).map(({ canonical_url }) => canonical_url); | ||
setCanonicalResults(canonical_urls); | ||
} catch (e) {} | ||
}); | ||
} | ||
}, [doResolveUris, stringifiedResults]); | ||
|
||
// Only resolve commentors on Livestreams if actually mentioning/looking for it | ||
React.useEffect(() => { | ||
if (isLivestream && unresolvedCommentors && mentionTerm) doResolveUris(unresolvedCommentors); | ||
}, [doResolveUris, isLivestream, mentionTerm, unresolvedCommentors]); | ||
|
||
// Only resolve the subscriptions that match the mention term, instead of all | ||
React.useEffect(() => { | ||
if (isTyping) return; | ||
|
||
const urisToResolve = []; | ||
subscriptionUris.map( | ||
(uri) => | ||
hasMinLength && | ||
possibleMatches.includes(uri) && | ||
unresolvedSubscriptions.includes(uri) && | ||
urisToResolve.push(uri) | ||
); | ||
|
||
if (urisToResolve.length > 0) doResolveUris(urisToResolve); | ||
}, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]); | ||
|
||
const suggestionsRow = ( | ||
label: string, | ||
suggestions: Array<string>, | ||
canonical: Array<string>, | ||
hasSuggestionsBelow: boolean | ||
) => { | ||
if (mentionTerm !== '@' && suggestions !== results) { | ||
suggestions = suggestions.filter((uri) => possibleMatches.includes(uri)); | ||
} else if (suggestions === results) { | ||
suggestions = suggestions.filter((uri) => !allShownUris.includes(uri)); | ||
} | ||
suggestions = suggestions | ||
.map((matchUri) => | ||
String( | ||
Boolean(canonical.find((uri) => matchUri.includes(uri))) && canonical.find((uri) => matchUri.includes(uri)) | ||
) | ||
) | ||
.filter((uri) => Boolean(uri)); | ||
|
||
return !suggestions.length ? null : ( | ||
<> | ||
<div className="channel-mention__label">{label}</div> | ||
{suggestions.map((uri) => ( | ||
<ChannelMentionSuggestion key={uri} uri={uri} /> | ||
))} | ||
{hasSuggestionsBelow && <hr className="channel-mention__top-separator" />} | ||
</> | ||
); | ||
}; | ||
|
||
return isInputFocused || isFocused ? ( | ||
<Form onSubmit={() => handleSelect(mentionTerm)}> | ||
<Combobox className="channel-mention" onSelect={handleSelect}> | ||
<ComboboxInput ref={comboboxInputRef} className="channel-mention__input--none" value={mentionTerm} /> | ||
{mentionTerm && isUriFromTermValid && ( | ||
<ComboboxPopover portal={false} className="channel-mention__suggestions"> | ||
<ComboboxList ref={comboboxListRef}> | ||
{creatorUri && | ||
suggestionsRow( | ||
__('Creator'), | ||
[creatorUri], | ||
canonicalCreator, | ||
hasSubscriptionsResolved || canonicalCommentors.length > 0 || !showPlaceholder | ||
)} | ||
{hasSubscriptionsResolved && | ||
suggestionsRow( | ||
__('Following'), | ||
subscriptionUris, | ||
canonicalSubscriptions, | ||
commentorUris.length > 0 || !showPlaceholder | ||
)} | ||
{commentorUris.length > 0 && | ||
suggestionsRow(__('From comments'), commentorUris, canonicalCommentors, !showPlaceholder)} | ||
|
||
{showPlaceholder | ||
? hasMinLength && <Spinner type="small" /> | ||
: results && ( | ||
<> | ||
{!noTopSuggestion && <ChannelMentionTopSuggestion query={debouncedTerm} />} | ||
{suggestionsRow(__('From search'), results, canonicalResults, false)} | ||
</> | ||
)} | ||
</ComboboxList> | ||
</ComboboxPopover> | ||
)} | ||
</Combobox> | ||
</Form> | ||
) : null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { connect } from 'react-redux'; | ||
import { makeSelectIsUriResolving, doResolveUri } from 'lbry-redux'; | ||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search'; | ||
import ChannelMentionTopSuggestion from './view'; | ||
|
||
const select = (state, props) => { | ||
const uriFromQuery = `lbry://${props.query}`; | ||
return { | ||
uriFromQuery, | ||
isResolvingUri: makeSelectIsUriResolving(uriFromQuery)(state), | ||
winningUri: makeSelectWinningUriForQuery(props.query)(state), | ||
}; | ||
}; | ||
|
||
export default connect(select, { doResolveUri })(ChannelMentionTopSuggestion); |
Oops, something went wrong.