feat(Avatar): CORS mode support.
This commit is contained in:
parent
32aa40b396
commit
12bc054386
|
@ -543,6 +543,9 @@ var config = {
|
||||||
// Document should be focused for this option to work
|
// Document should be focused for this option to work
|
||||||
// enableAutomaticUrlCopy: false,
|
// enableAutomaticUrlCopy: false,
|
||||||
|
|
||||||
|
// Array with avatar URL prefixes that need to use CORS.
|
||||||
|
// corsAvatarURLs: [ 'https://www.gravatar.com/avatar/' ],
|
||||||
|
|
||||||
// Base URL for a Gravatar-compatible service. Defaults to libravatar.
|
// Base URL for a Gravatar-compatible service. Defaults to libravatar.
|
||||||
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/',
|
// gravatarBaseURL: 'https://seccdn.libravatar.org/avatar/',
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,11 @@ export type Props = {
|
||||||
*/
|
*/
|
||||||
onAvatarLoadError?: Function,
|
onAvatarLoadError?: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional parameters to be passed to onAvatarLoadError function.
|
||||||
|
*/
|
||||||
|
onAvatarLoadErrorParams?: Object,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expected size of the avatar.
|
* Expected size of the avatar.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,12 +4,17 @@ import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import { getParticipantById } from '../../participants';
|
import { getParticipantById } from '../../participants';
|
||||||
import { connect } from '../../redux';
|
import { connect } from '../../redux';
|
||||||
import { getAvatarColor, getInitials } from '../functions';
|
import { getAvatarColor, getInitials, isCORSAvatarURL } from '../functions';
|
||||||
|
|
||||||
import { StatelessAvatar } from '.';
|
import { StatelessAvatar } from '.';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL patterns for URLs that needs to be handled with CORS.
|
||||||
|
*/
|
||||||
|
_corsAvatarURLs: Array<string>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom avatar backgrounds from branding.
|
* Custom avatar backgrounds from branding.
|
||||||
*/
|
*/
|
||||||
|
@ -25,6 +30,11 @@ export type Props = {
|
||||||
*/
|
*/
|
||||||
_loadableAvatarUrl: ?string,
|
_loadableAvatarUrl: ?string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether _loadableAvatarUrl should use CORS or not.
|
||||||
|
*/
|
||||||
|
_loadableAvatarUrlUseCORS: ?boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A prop to maintain compatibility with web.
|
* A prop to maintain compatibility with web.
|
||||||
*/
|
*/
|
||||||
|
@ -76,10 +86,16 @@ export type Props = {
|
||||||
* URL of the avatar, if any.
|
* URL of the avatar, if any.
|
||||||
*/
|
*/
|
||||||
url: ?string,
|
url: ?string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to load the avatar using CORS or not.
|
||||||
|
*/
|
||||||
|
useCORS?: ?boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
avatarFailed: boolean
|
avatarFailed: boolean,
|
||||||
|
isUsingCORS: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIZE = 65;
|
export const DEFAULT_SIZE = 65;
|
||||||
|
@ -105,8 +121,15 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
constructor(props: P) {
|
constructor(props: P) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const {
|
||||||
|
_corsAvatarURLs,
|
||||||
|
url,
|
||||||
|
useCORS
|
||||||
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
avatarFailed: false
|
avatarFailed: false,
|
||||||
|
isUsingCORS: Boolean(useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||||
};
|
};
|
||||||
|
|
||||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||||
|
@ -118,7 +141,9 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
componentDidUpdate(prevProps: P) {
|
componentDidUpdate(prevProps: P) {
|
||||||
if (prevProps.url !== this.props.url) {
|
const { _corsAvatarURLs, url } = this.props;
|
||||||
|
|
||||||
|
if (prevProps.url !== url) {
|
||||||
|
|
||||||
// URI changed, so we need to try to fetch it again.
|
// URI changed, so we need to try to fetch it again.
|
||||||
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
|
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
|
||||||
|
@ -126,7 +151,8 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
|
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarFailed: false
|
avatarFailed: false,
|
||||||
|
isUsingCORS: Boolean(this.props.useCORS) || Boolean(url && isCORSAvatarURL(url, _corsAvatarURLs))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +167,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
_customAvatarBackgrounds,
|
_customAvatarBackgrounds,
|
||||||
_initialsBase,
|
_initialsBase,
|
||||||
_loadableAvatarUrl,
|
_loadableAvatarUrl,
|
||||||
|
_loadableAvatarUrlUseCORS,
|
||||||
className,
|
className,
|
||||||
colorBase,
|
colorBase,
|
||||||
dynamicColor,
|
dynamicColor,
|
||||||
|
@ -150,7 +177,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
testId,
|
testId,
|
||||||
url
|
url
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { avatarFailed } = this.state;
|
const { avatarFailed, isUsingCORS } = this.state;
|
||||||
|
|
||||||
const avatarProps = {
|
const avatarProps = {
|
||||||
className,
|
className,
|
||||||
|
@ -158,19 +185,26 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
id,
|
id,
|
||||||
initials: undefined,
|
initials: undefined,
|
||||||
onAvatarLoadError: undefined,
|
onAvatarLoadError: undefined,
|
||||||
|
onAvatarLoadErrorParams: undefined,
|
||||||
size,
|
size,
|
||||||
status,
|
status,
|
||||||
testId,
|
testId,
|
||||||
url: undefined
|
url: undefined,
|
||||||
|
useCORS: isUsingCORS
|
||||||
};
|
};
|
||||||
|
|
||||||
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
|
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
|
||||||
// we still need to do a check for that. And an explicitly provided URI is higher priority than
|
// we still need to do a check for that. And an explicitly provided URI is higher priority than
|
||||||
// an avatar URL anyhow.
|
// an avatar URL anyhow.
|
||||||
const effectiveURL = (!avatarFailed && url) || _loadableAvatarUrl;
|
const useReduxLoadableAvatarURL = avatarFailed || !url;
|
||||||
|
const effectiveURL = useReduxLoadableAvatarURL ? _loadableAvatarUrl : url;
|
||||||
|
|
||||||
if (effectiveURL) {
|
if (effectiveURL) {
|
||||||
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
|
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
|
||||||
|
if (useReduxLoadableAvatarURL) {
|
||||||
|
avatarProps.onAvatarLoadErrorParams = { dontRetry: true };
|
||||||
|
avatarProps.useCORS = _loadableAvatarUrlUseCORS;
|
||||||
|
}
|
||||||
avatarProps.url = effectiveURL;
|
avatarProps.url = effectiveURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,13 +229,25 @@ class Avatar<P: Props> extends PureComponent<P, State> {
|
||||||
/**
|
/**
|
||||||
* Callback to handle the error while loading of the avatar URI.
|
* Callback to handle the error while loading of the avatar URI.
|
||||||
*
|
*
|
||||||
|
* @param {Object} params - An object with parameters.
|
||||||
|
* @param {boolean} params.dontRetry - If false we will retry to load the Avatar with different CORS mode.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_onAvatarLoadError() {
|
_onAvatarLoadError(params = {}) {
|
||||||
|
const { dontRetry = false } = params;
|
||||||
|
|
||||||
|
if (Boolean(this.props.useCORS) === this.state.isUsingCORS && !dontRetry) {
|
||||||
|
// try different mode of loading the avatar.
|
||||||
|
this.setState({
|
||||||
|
isUsingCORS: !this.state.isUsingCORS
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// we already have tried loading the avatar with and without CORS and it failed.
|
||||||
this.setState({
|
this.setState({
|
||||||
avatarFailed: true
|
avatarFailed: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -215,11 +261,14 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||||
const { colorBase, displayName, participantId } = ownProps;
|
const { colorBase, displayName, participantId } = ownProps;
|
||||||
const _participant: ?Object = participantId && getParticipantById(state, participantId);
|
const _participant: ?Object = participantId && getParticipantById(state, participantId);
|
||||||
const _initialsBase = _participant?.name ?? displayName;
|
const _initialsBase = _participant?.name ?? displayName;
|
||||||
|
const { corsAvatarURLs } = state['features/base/config'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
|
||||||
|
_corsAvatarURLs: corsAvatarURLs,
|
||||||
_initialsBase,
|
_initialsBase,
|
||||||
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
|
||||||
|
_loadableAvatarUrlUseCORS: _participant?.loadableAvatarUrlUseCORS,
|
||||||
colorBase
|
colorBase
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,18 @@ type Props = AbstractProps & {
|
||||||
* props.
|
* props.
|
||||||
*/
|
*/
|
||||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Component}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements {@code Component#render}.
|
* Implements {@code Component#render}.
|
||||||
*
|
*
|
||||||
|
@ -164,4 +176,22 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
style = { styles.avatarContent(size) } />
|
style = { styles.avatarContent(size) } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onAvatarLoadError: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles avatar load errors.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onAvatarLoadError() {
|
||||||
|
const { onAvatarLoadError, onAvatarLoadErrorParams = {} } = this.props;
|
||||||
|
|
||||||
|
if (onAvatarLoadError) {
|
||||||
|
onAvatarLoadError({
|
||||||
|
...onAvatarLoadErrorParams,
|
||||||
|
dontRetry: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Icon } from '../../../icons';
|
import { Icon } from '../../../icons';
|
||||||
import { isGravatarURL } from '../../functions';
|
|
||||||
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
|
||||||
|
|
||||||
type Props = AbstractProps & {
|
type Props = AbstractProps & {
|
||||||
|
@ -31,7 +30,12 @@ type Props = AbstractProps & {
|
||||||
/**
|
/**
|
||||||
* TestId of the element, if any.
|
* TestId of the element, if any.
|
||||||
*/
|
*/
|
||||||
testId?: string
|
testId?: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether to load the avatar using CORS or not.
|
||||||
|
*/
|
||||||
|
useCORS?: ?boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,13 +43,25 @@ type Props = AbstractProps & {
|
||||||
* props.
|
* props.
|
||||||
*/
|
*/
|
||||||
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Component}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements {@code Component#render}.
|
* Implements {@code Component#render}.
|
||||||
*
|
*
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { initials, url } = this.props;
|
const { initials, url, useCORS } = this.props;
|
||||||
|
|
||||||
if (this._isIcon(url)) {
|
if (this._isIcon(url)) {
|
||||||
return (
|
return (
|
||||||
|
@ -67,10 +83,10 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
<img
|
<img
|
||||||
alt = 'avatar'
|
alt = 'avatar'
|
||||||
className = { this._getAvatarClassName() }
|
className = { this._getAvatarClassName() }
|
||||||
crossOrigin = { isGravatarURL(url) ? '' : undefined }
|
crossOrigin = { useCORS ? '' : undefined }
|
||||||
data-testid = { this.props.testId }
|
data-testid = { this.props.testId }
|
||||||
id = { this.props.id }
|
id = { this.props.id }
|
||||||
onError = { this.props.onAvatarLoadError }
|
onError = { this._onAvatarLoadError }
|
||||||
src = { url }
|
src = { url }
|
||||||
style = { this._getAvatarStyle() } />
|
style = { this._getAvatarStyle() } />
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,4 +176,19 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_isIcon: (?string | ?Object) => boolean;
|
_isIcon: (?string | ?Object) => boolean;
|
||||||
|
|
||||||
|
_onAvatarLoadError: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles avatar load errors.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onAvatarLoadError() {
|
||||||
|
const { onAvatarLoadError, onAvatarLoadErrorParams } = this.props;
|
||||||
|
|
||||||
|
if (typeof onAvatarLoadError === 'function') {
|
||||||
|
onAvatarLoadError(onAvatarLoadErrorParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
import GraphemeSplitter from 'grapheme-splitter';
|
import GraphemeSplitter from 'grapheme-splitter';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { GRAVATAR_BASE_URL } from './constants';
|
|
||||||
|
|
||||||
const AVATAR_COLORS = [
|
const AVATAR_COLORS = [
|
||||||
'#6A50D3',
|
'#6A50D3',
|
||||||
'#FF9B42',
|
'#FF9B42',
|
||||||
|
@ -74,11 +72,12 @@ export function getInitials(s: ?string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the passed URL is pointing to the gravatar service.
|
* Checks if the passed URL should be loaded with CORS.
|
||||||
*
|
*
|
||||||
* @param {string} url - The URL.
|
* @param {string} url - The URL.
|
||||||
|
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
export function isGravatarURL(url: string = '') {
|
export function isCORSAvatarURL(url: string | any = '', corsURLs: Array<string> = []) {
|
||||||
return url.startsWith(GRAVATAR_BASE_URL);
|
return corsURLs.some(pattern => url.startsWith(pattern));
|
||||||
}
|
}
|
||||||
|
|
|
@ -540,20 +540,23 @@ export function pinParticipant(id) {
|
||||||
*
|
*
|
||||||
* @param {string} participantId - The ID of the participant.
|
* @param {string} participantId - The ID of the participant.
|
||||||
* @param {string} url - The new URL.
|
* @param {string} url - The new URL.
|
||||||
|
* @param {boolean} useCORS - Indicates whether we need to use CORS for this URL.
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: SET_LOADABLE_AVATAR_URL,
|
* type: SET_LOADABLE_AVATAR_URL,
|
||||||
* participant: {
|
* participant: {
|
||||||
* id: string,
|
* id: string,
|
||||||
* loadableAvatarUrl: string
|
* loadableAvatarUrl: string,
|
||||||
|
* loadableAvatarUrlUseCORS: boolean
|
||||||
* }
|
* }
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function setLoadableAvatarUrl(participantId, url) {
|
export function setLoadableAvatarUrl(participantId, url, useCORS) {
|
||||||
return {
|
return {
|
||||||
type: SET_LOADABLE_AVATAR_URL,
|
type: SET_LOADABLE_AVATAR_URL,
|
||||||
participant: {
|
participant: {
|
||||||
id: participantId,
|
id: participantId,
|
||||||
loadableAvatarUrl: url
|
loadableAvatarUrl: url,
|
||||||
|
loadableAvatarUrlUseCORS: useCORS
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||||
import type { Store } from 'redux';
|
import type { Store } from 'redux';
|
||||||
|
|
||||||
import { GRAVATAR_BASE_URL } from '../avatar';
|
import { GRAVATAR_BASE_URL, isCORSAvatarURL } from '../avatar';
|
||||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||||
import { toState } from '../redux';
|
import { toState } from '../redux';
|
||||||
|
@ -56,7 +56,7 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
||||||
const deferred = createDeferred();
|
const deferred = createDeferred();
|
||||||
const fullPromise = deferred.promise
|
const fullPromise = deferred.promise
|
||||||
.then(() => _getFirstLoadableAvatarUrl(participant, store))
|
.then(() => _getFirstLoadableAvatarUrl(participant, store))
|
||||||
.then(src => {
|
.then(result => {
|
||||||
|
|
||||||
if (AVATAR_QUEUE.length) {
|
if (AVATAR_QUEUE.length) {
|
||||||
const next = AVATAR_QUEUE.shift();
|
const next = AVATAR_QUEUE.shift();
|
||||||
|
@ -64,7 +64,7 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
|
||||||
next.resolve();
|
next.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return src;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (AVATAR_QUEUE.length) {
|
if (AVATAR_QUEUE.length) {
|
||||||
|
@ -432,18 +432,33 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
|
||||||
|
|
||||||
if (url !== null) {
|
if (url !== null) {
|
||||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||||
if (AVATAR_CHECKED_URLS.get(url)) {
|
const { isLoadable, isUsingCORS } = AVATAR_CHECKED_URLS.get(url) || {};
|
||||||
return url;
|
|
||||||
|
if (isLoadable) {
|
||||||
|
return {
|
||||||
|
isUsingCORS,
|
||||||
|
src: url
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const finalUrl = await preloadImage(url);
|
const { corsAvatarURLs } = store.getState()['features/base/config'];
|
||||||
|
const { isUsingCORS, src } = await preloadImage(url, isCORSAvatarURL(url, corsAvatarURLs));
|
||||||
|
|
||||||
AVATAR_CHECKED_URLS.set(finalUrl, true);
|
AVATAR_CHECKED_URLS.set(src, {
|
||||||
|
isLoadable: true,
|
||||||
|
isUsingCORS
|
||||||
|
});
|
||||||
|
|
||||||
return finalUrl;
|
return {
|
||||||
|
isUsingCORS,
|
||||||
|
src
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AVATAR_CHECKED_URLS.set(url, false);
|
AVATAR_CHECKED_URLS.set(url, {
|
||||||
|
isLoadable: false,
|
||||||
|
isUsingCORS: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -488,8 +488,8 @@ function _participantJoinedOrUpdated(store, next, action) {
|
||||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||||
|
|
||||||
getFirstLoadableAvatarUrl(updatedParticipant, store)
|
getFirstLoadableAvatarUrl(updatedParticipant, store)
|
||||||
.then(url => {
|
.then(urlData => {
|
||||||
dispatch(setLoadableAvatarUrl(participantId, url));
|
dispatch(setLoadableAvatarUrl(participantId, urlData?.src, urlData?.isUsingCORS));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,17 @@ import { isIconUrl } from './functions';
|
||||||
* @param {string | Object} src - Source of the avatar.
|
* @param {string | Object} src - Source of the avatar.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function preloadImage(src: string | Object): Promise<string> {
|
export function preloadImage(src: string | Object): Promise<Object> {
|
||||||
if (isIconUrl(src)) {
|
if (isIconUrl(src)) {
|
||||||
return Promise.resolve(src);
|
return Promise.resolve(src);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Image.prefetch(src).then(() => resolve(src), reject);
|
Image.prefetch(src).then(
|
||||||
|
() => resolve({
|
||||||
|
src,
|
||||||
|
isUsingCORS: false
|
||||||
|
}),
|
||||||
|
reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,45 @@
|
||||||
|
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { isGravatarURL } from '../avatar';
|
|
||||||
|
|
||||||
import { isIconUrl } from './functions';
|
import { isIconUrl } from './functions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to preload an image.
|
* Tries to preload an image.
|
||||||
*
|
*
|
||||||
* @param {string | Object} src - Source of the avatar.
|
* @param {string | Object} src - Source of the avatar.
|
||||||
|
* @param {boolean} useCORS - Whether to use CORS or not.
|
||||||
|
* @param {boolean} tryOnce - If true we try to load the image only using the specified CORS mode. Otherwise both modes
|
||||||
|
* (CORS and no CORS) will be used to load the image if the first atempt fails.
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function preloadImage(src: string | Object): Promise<string> {
|
export function preloadImage(
|
||||||
|
src: string | Object,
|
||||||
|
useCORS: ?boolean = false,
|
||||||
|
tryOnce: ?boolean = false
|
||||||
|
): Promise<Object> {
|
||||||
if (isIconUrl(src)) {
|
if (isIconUrl(src)) {
|
||||||
return Promise.resolve(src);
|
return Promise.resolve({ src });
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const image = document.createElement('img');
|
const image = document.createElement('img');
|
||||||
|
|
||||||
if (isGravatarURL(src)) {
|
if (useCORS) {
|
||||||
image.setAttribute('crossOrigin', '');
|
image.setAttribute('crossOrigin', '');
|
||||||
}
|
}
|
||||||
image.onload = () => resolve(src);
|
image.onload = () => resolve({
|
||||||
image.onerror = reject;
|
src,
|
||||||
|
isUsingCORS: useCORS
|
||||||
|
});
|
||||||
|
image.onerror = error => {
|
||||||
|
if (tryOnce) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
preloadImage(src, !useCORS, true)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// $FlowExpectedError
|
// $FlowExpectedError
|
||||||
image.referrerPolicy = 'no-referrer';
|
image.referrerPolicy = 'no-referrer';
|
||||||
|
|
|
@ -193,12 +193,15 @@ function _findLoadableAvatarForKnockingParticipant(store, { id }) {
|
||||||
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
||||||
|
|
||||||
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
|
||||||
getFirstLoadableAvatarUrl(updatedParticipant, store).then(loadableAvatarUrl => {
|
getFirstLoadableAvatarUrl(updatedParticipant, store).then(result => {
|
||||||
if (loadableAvatarUrl) {
|
if (result) {
|
||||||
|
const { isUsingCORS, src } = result;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
participantIsKnockingOrUpdated({
|
participantIsKnockingOrUpdated({
|
||||||
loadableAvatarUrl,
|
loadableAvatarUrl: src,
|
||||||
id
|
id,
|
||||||
|
isUsingCORS
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue