feat: LTI support and Moodle plugin

This commit is contained in:
Markos Gogoulos
2026-05-11 12:47:09 +03:00
committed by GitHub
parent b7427869b6
commit 55ab7ff34f
307 changed files with 19966 additions and 3748 deletions
@@ -5,7 +5,7 @@ import { LinksContext, MemberContext, SiteContext } from '../../utils/contexts/'
import { PageStore, ProfilePageStore } from '../../utils/stores/';
import { PageActions, ProfilePageActions } from '../../utils/actions/';
import { CircleIconButton, PopupMain } from '../_shared';
import { translateString } from '../../utils/helpers/';
import { translateString, inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode, isShareMediaDisabled } from '../../utils/helpers/';
class ProfileSearchBar extends React.PureComponent {
constructor(props) {
@@ -372,30 +372,51 @@ class NavMenuInlineTabs extends React.PureComponent {
}
render() {
const isSelectMediaMode = inSelectMediaEmbedMode();
const shareMediaDisabled = isShareMediaDisabled();
// Append action=select_media to links when in select mode
const mediaLink = isSelectMediaMode
? `${LinksContext._currentValue.profile.media}${LinksContext._currentValue.profile.media.includes('?') ? '&' : '?'}action=select_media`
: LinksContext._currentValue.profile.media;
const sharedByMeLink = isSelectMediaMode
? `${LinksContext._currentValue.profile.shared_by_me}${LinksContext._currentValue.profile.shared_by_me.includes('?') ? '&' : '?'}action=select_media`
: LinksContext._currentValue.profile.shared_by_me;
const sharedWithMeBase = shareMediaDisabled
? `${LinksContext._currentValue.profile.shared_with_me}${LinksContext._currentValue.profile.shared_with_me.includes('?') ? '&' : '?'}share_media=0`
: LinksContext._currentValue.profile.shared_with_me;
const sharedWithMeLink = isSelectMediaMode
? `${sharedWithMeBase}${sharedWithMeBase.includes('?') ? '&' : '?'}action=select_media`
: sharedWithMeBase;
return (
<nav ref="tabsNav" className="profile-nav items-list-outer list-inline list-slider">
<div className="profile-nav-inner items-list-outer">
{this.state.displayPrev ? this.previousBtn : null}
<ul className="items-list-wrap" ref="itemsListWrap">
<InlineTab
id="about"
isActive={'about' === this.props.type}
label={translateString('About')}
link={LinksContext._currentValue.profile.about}
/>
{!isSelectMediaMode && !inEmbeddedApp() ? (
<InlineTab
id="about"
isActive={'about' === this.props.type}
label={translateString('About')}
link={LinksContext._currentValue.profile.about}
/>
) : null}
<InlineTab
id="media"
isActive={'media' === this.props.type}
label={translateString(this.userIsAuthor ? 'Media I own' : 'Media')}
link={LinksContext._currentValue.profile.media}
link={mediaLink}
/>
{this.userIsAuthor ? (
<InlineTab
id="shared_by_me"
isActive={'shared_by_me' === this.props.type}
label={translateString('Shared by me')}
link={LinksContext._currentValue.profile.shared_by_me}
link={sharedByMeLink}
/>
) : null}
{this.userIsAuthor ? (
@@ -403,11 +424,11 @@ class NavMenuInlineTabs extends React.PureComponent {
id="shared_with_me"
isActive={'shared_with_me' === this.props.type}
label={translateString('Shared with me')}
link={LinksContext._currentValue.profile.shared_with_me}
link={sharedWithMeLink}
/>
) : null}
{MemberContext._currentValue.can.saveMedia ? (
{!isSelectMediaMode && MemberContext._currentValue.can.saveMedia ? (
<InlineTab
id="playlists"
isActive={'playlists' === this.props.type}
@@ -474,6 +495,39 @@ class NavMenuInlineTabs extends React.PureComponent {
</span>
</li>
) : null}
{this.props.onToggleSharingClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-sharing-toggle">
<span
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
position: 'relative',
}}
onClick={this.props.onToggleSharingClick}
title={translateString('Shared with')}
>
<CircleIconButton buttonShadow={false}>
<i className="material-icons">people</i>
</CircleIconButton>
{this.props.hasActiveSharing ? (
<span
style={{
position: 'absolute',
top: '8px',
right: '8px',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: 'var(--default-theme-color)',
border: '2px solid white',
}}
></span>
) : null}
</span>
</li>
) : null}
{this.props.onToggleTagsClick &&
['media', 'shared_by_me', 'shared_with_me'].includes(this.props.type) ? (
<li className="media-tags-toggle">
@@ -553,9 +607,11 @@ NavMenuInlineTabs.propTypes = {
type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};
@@ -606,12 +662,6 @@ export default function ProfilePagesHeader(props) {
const profilePageHeaderRef = useRef(null);
const profileNavRef = useRef(null);
const [fixedNav, setFixedNav] = useState(false);
const positions = {
profileNavTop: 0,
};
const userIsAdmin = !MemberContext._currentValue.is.anonymous && MemberContext._currentValue.is.admin;
const userIsAuthor =
!MemberContext._currentValue.is.anonymous &&
@@ -623,18 +673,6 @@ export default function ProfilePagesHeader(props) {
userIsAuthor ||
(!MemberContext._currentValue.is.anonymous && MemberContext._currentValue.can.deleteProfile);
function updateProfileNavTopPosition() {
positions.profileHeaderTop = profilePageHeaderRef.current.offsetTop;
positions.profileNavTop =
positions.profileHeaderTop +
profilePageHeaderRef.current.offsetHeight -
profileNavRef.current.refs.tabsNav.offsetHeight;
}
function updateFixedNavPosition() {
setFixedNav(positions.profileHeaderTop + window.scrollY > positions.profileNavTop);
}
function cancelProfileRemoval() {
popupContentRef.current.toggle();
}
@@ -669,42 +707,26 @@ export default function ProfilePagesHeader(props) {
}
}
function onWindowResize() {
updateProfileNavTopPosition();
updateFixedNavPosition();
}
function onWindowScroll() {
updateFixedNavPosition();
}
useEffect(() => {
if (userCanDeleteProfile) {
ProfilePageStore.on('profile_delete', onProfileDelete);
ProfilePageStore.on('profile_delete_fail', onProfileDeleteFail);
}
PageStore.on('resize', onWindowResize);
PageStore.on('changed_page_sidebar_visibility', onWindowResize);
PageStore.on('window_scroll', onWindowScroll);
updateProfileNavTopPosition();
updateFixedNavPosition();
return () => {
if (userCanDeleteProfile) {
ProfilePageStore.removeListener('profile_delete', onProfileDelete);
ProfilePageStore.removeListener('profile_delete_fail', onProfileDeleteFail);
}
PageStore.removeListener('resize', onWindowResize);
PageStore.removeListener('changed_page_sidebar_visibility', onWindowResize);
PageStore.removeListener('window_scroll', onWindowScroll);
};
}, []);
return (
<div ref={profilePageHeaderRef} className={'profile-page-header' + (fixedNav ? ' fixed-nav' : '')}>
<div
ref={profilePageHeaderRef}
className={'profile-page-header'}
{...(isSelectMediaMode() ? { 'data-action': 'select_media' } : {})}
>
{!props.hideChannelBanner && (
<span className="profile-banner-wrap">
{props.author.banner_thumbnail_url ? (
@@ -768,7 +790,7 @@ export default function ProfilePagesHeader(props) {
)}
<div className="profile-info-nav-wrap">
{props.author.thumbnail_url || props.author.name ? (
{!inEmbeddedApp() && (props.author.thumbnail_url || props.author.name) ? (
<div className="profile-info">
<div className="profile-info-inner">
<div>
@@ -793,9 +815,11 @@ export default function ProfilePagesHeader(props) {
type={props.type}
onQueryChange={props.onQueryChange}
onToggleFiltersClick={props.onToggleFiltersClick}
onToggleSharingClick={userIsAuthor ? props.onToggleSharingClick : undefined}
onToggleTagsClick={props.onToggleTagsClick}
onToggleSortingClick={props.onToggleSortingClick}
hasActiveFilters={props.hasActiveFilters}
hasActiveSharing={props.hasActiveSharing}
hasActiveTags={props.hasActiveTags}
hasActiveSort={props.hasActiveSort}
/>
@@ -809,9 +833,11 @@ ProfilePagesHeader.propTypes = {
type: PropTypes.string.isRequired,
onQueryChange: PropTypes.func,
onToggleFiltersClick: PropTypes.func,
onToggleSharingClick: PropTypes.func,
onToggleTagsClick: PropTypes.func,
onToggleSortingClick: PropTypes.func,
hasActiveFilters: PropTypes.bool,
hasActiveSharing: PropTypes.bool,
hasActiveTags: PropTypes.bool,
hasActiveSort: PropTypes.bool,
};