Notifications: use fetched urls instead of resolving (#820)

* Fix avatar occasionally stuck in spaceman

## Issue
Sometimes, we'll see a channel profile (e.g. in upper-right, in channel selector) stuck in the fallback Spaceman image.

This is due to OptimizedImage always starting with a blank src, and updated later when the mounted size has been determined. ChannelThumbnail, which uses OptimizedImage, captured the `onError` due to blank src.

## Fix
Don't mount the <img> until the optimum size has been determined.

* FileThumbnail: skip resolve if thumbnail url is specified

* UriIndicator: skip resolve if channel info is specified

* Notifications: disable batch resolve + use fetched data if available + fallback to resolve if n/a

The fallback is using the individual resolve when no direct data is provided and claim is undefined.
This commit is contained in:
infinite-persistence 2022-02-07 12:59:20 -08:00 committed by GitHub
parent d73504d69c
commit 1e7e7a7b7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 30 deletions

View file

@ -115,11 +115,14 @@ declare type WebNotification = {
}, },
dynamic: { dynamic: {
comment_author: string, comment_author: string,
comment_author_thumbnail?: string,
reply_author: string, reply_author: string,
hash: string, hash: string,
claim_title: string, claim_title: string,
claim_thumbnail?: string,
comment?: string, comment?: string,
channel_url: string, channel_url: string,
channel_thumbnail?: string,
}, },
email: {}, email: {},
}, },

View file

@ -30,10 +30,10 @@ function FileThumbnail(props: Props) {
const isGif = thumbnail && thumbnail.endsWith('gif'); const isGif = thumbnail && thumbnail.endsWith('gif');
React.useEffect(() => { React.useEffect(() => {
if (!hasResolvedClaim && uri) { if (!hasResolvedClaim && uri && !passedThumbnail) {
doResolveUri(uri); doResolveUri(uri);
} }
}, [hasResolvedClaim, uri, doResolveUri]); }, [hasResolvedClaim, uri, doResolveUri, passedThumbnail]);
if (!allowGifs && isGif) { if (!allowGifs && isGif) {
return ( return (

View file

@ -50,11 +50,12 @@ export default function Notification(props: Props) {
const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText); const stickerFromComment = isCommentNotification && commentText && parseSticker(commentText);
const notificationTarget = getNotificationTarget(); const notificationTarget = getNotificationTarget();
const creatorIcon = (channelUrl) => ( const creatorIcon = (channelUrl, channelThumbnail) => (
<UriIndicator uri={channelUrl} link> <UriIndicator uri={channelUrl} link channelInfo={{ uri: channelUrl, name: '' }}>
<ChannelThumbnail small uri={channelUrl} /> <ChannelThumbnail small thumbnailPreview={channelThumbnail} uri={channelThumbnail ? undefined : channelUrl} />
</UriIndicator> </UriIndicator>
); );
let channelUrl; let channelUrl;
let icon; let icon;
switch (notification_rule) { switch (notification_rule) {
@ -64,19 +65,19 @@ export default function Notification(props: Props) {
case RULE.COMMENT: case RULE.COMMENT:
case RULE.CREATOR_COMMENT: case RULE.CREATOR_COMMENT:
channelUrl = notification_parameters.dynamic.comment_author; channelUrl = notification_parameters.dynamic.comment_author;
icon = creatorIcon(channelUrl); icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.comment_author_thumbnail);
break; break;
case RULE.COMMENT_REPLY: case RULE.COMMENT_REPLY:
channelUrl = notification_parameters.dynamic.reply_author; channelUrl = notification_parameters.dynamic.reply_author;
icon = creatorIcon(channelUrl); icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.comment_author_thumbnail);
break; break;
case RULE.NEW_CONTENT: case RULE.NEW_CONTENT:
channelUrl = notification_parameters.dynamic.channel_url; channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl); icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.channel_thumbnail);
break; break;
case RULE.NEW_LIVESTREAM: case RULE.NEW_LIVESTREAM:
channelUrl = notification_parameters.dynamic.channel_url; channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl); icon = creatorIcon(channelUrl, notification_parameters?.dynamic?.channel_thumbnail);
break; break;
case RULE.DAILY_WATCH_AVAILABLE: case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND: case RULE.DAILY_WATCH_REMIND:
@ -111,7 +112,7 @@ export default function Notification(props: Props) {
let uriIndicator; let uriIndicator;
const title = titleSplit.map((message, index) => { const title = titleSplit.map((message, index) => {
if (channelName === message) { if (channelName === message) {
uriIndicator = <UriIndicator uri={channelUrl} link />; uriIndicator = <UriIndicator uri={channelUrl} link channelInfo={{ uri: channelUrl, name: channelName }} />;
fullTitle.push(' '); fullTitle.push(' ');
const resultTitle = fullTitle; const resultTitle = fullTitle;
fullTitle = [' ']; fullTitle = [' '];
@ -204,7 +205,11 @@ export default function Notification(props: Props) {
</div> </div>
{notification_rule === RULE.NEW_CONTENT && ( {notification_rule === RULE.NEW_CONTENT && (
<FileThumbnail uri={notification_parameters.device.target} className="notificationContent__thumbnail" /> <FileThumbnail
uri={notification_parameters.device.target}
thumbnail={notification_parameters?.dynamic?.claim_thumbnail}
className="notificationContent__thumbnail"
/>
)} )}
{notification_rule === RULE.NEW_LIVESTREAM && ( {notification_rule === RULE.NEW_LIVESTREAM && (
<FileThumbnail <FileThumbnail

View file

@ -94,7 +94,7 @@ function OptimizedImage(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [src]); }, [src]);
if (!src) { if (!src || !optimizedSrc) {
return null; return null;
} }

View file

@ -4,9 +4,13 @@ import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Button from 'component/button'; import Button from 'component/button';
type ChannelInfo = {
uri: string,
name: string,
};
type Props = { type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
channelUri: ?string,
link: ?boolean, link: ?boolean,
claim: ?Claim, claim: ?Claim,
hideAnonymous: boolean, hideAnonymous: boolean,
@ -14,6 +18,7 @@ type Props = {
// Possibly because the resolve function is an arrow function that is passed in props? // Possibly because the resolve function is an arrow function that is passed in props?
resolveUri: (string) => void, resolveUri: (string) => void,
uri: string, uri: string,
channelInfo: ?ChannelInfo, // Direct channel info to use, bypassing the need to resolve 'uri'.
// to allow for other elements to be nested within the UriIndicator // to allow for other elements to be nested within the UriIndicator
children: ?Node, children: ?Node,
inline: boolean, inline: boolean,
@ -24,23 +29,53 @@ type Props = {
class UriIndicator extends React.PureComponent<Props> { class UriIndicator extends React.PureComponent<Props> {
componentDidMount() { componentDidMount() {
this.resolve(this.props); this.resolveClaim(this.props);
} }
componentDidUpdate() { componentDidUpdate() {
this.resolve(this.props); this.resolveClaim(this.props);
} }
resolve = (props: Props) => { resolveClaim = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri } = props; const { isResolvingUri, resolveUri, claim, uri, channelInfo } = props;
if (!isResolvingUri && claim === undefined && uri) { if (!channelInfo && !isResolvingUri && claim === undefined && uri) {
resolveUri(uri); resolveUri(uri);
} }
}; };
resolveState = (channelInfo: ?ChannelInfo, claim: ?Claim, isLinkType: ?boolean) => {
if (channelInfo) {
return {
hasChannelData: true,
isAnonymous: false,
channelName: channelInfo.name,
channelLink: isLinkType ? channelInfo.uri : false,
};
} else if (claim) {
const signingChannel = claim.signing_channel && claim.signing_channel.amount;
const isChannelClaim = claim.value_type === 'channel';
const channelClaim = isChannelClaim ? claim : claim.signing_channel;
return {
hasChannelData: Boolean(channelClaim),
isAnonymous: !signingChannel && !isChannelClaim,
channelName: channelClaim?.name,
channelLink: isLinkType ? channelClaim?.canonical_url || channelClaim?.permanent_url : false,
};
} else {
return {
hasChannelData: false,
isAnonymous: undefined,
channelName: undefined,
channelLink: undefined,
};
}
};
render() { render() {
const { const {
channelInfo,
link, link,
isResolvingUri, isResolvingUri,
claim, claim,
@ -52,7 +87,7 @@ class UriIndicator extends React.PureComponent<Props> {
className, className,
} = this.props; } = this.props;
if (!claim) { if (!channelInfo && !claim) {
return ( return (
<span className={classnames('empty', className)}> <span className={classnames('empty', className)}>
{isResolvingUri || claim === undefined ? __('Validating...') : __('[Removed]')} {isResolvingUri || claim === undefined ? __('Validating...') : __('[Removed]')}
@ -60,9 +95,9 @@ class UriIndicator extends React.PureComponent<Props> {
); );
} }
const isChannelClaim = claim.value_type === 'channel'; const data = this.resolveState(channelInfo, claim, link);
const signingChannel = claim.signing_channel && claim.signing_channel.amount;
if (!signingChannel && !isChannelClaim) { if (data.isAnonymous) {
if (hideAnonymous) { if (hideAnonymous) {
return null; return null;
} }
@ -74,15 +109,12 @@ class UriIndicator extends React.PureComponent<Props> {
); );
} }
const channelClaim = isChannelClaim ? claim : claim.signing_channel; if (data.hasChannelData) {
const { channelName, channelLink } = data;
if (channelClaim) {
const { name } = channelClaim;
const channelLink = link ? channelClaim.canonical_url || channelClaim.permanent_url : false;
const inner = ( const inner = (
<span dir="auto" className={classnames('channel-name', { 'channel-name--inline': inline })}> <span dir="auto" className={classnames('channel-name', { 'channel-name--inline': inline })}>
{name} {channelName}
</span> </span>
); );

View file

@ -22,7 +22,7 @@ type Props = {
unseenCount: number, unseenCount: number,
doSeeAllNotifications: () => void, doSeeAllNotifications: () => void,
doReadNotifications: () => void, doReadNotifications: () => void,
doNotificationList: (?Array<string>) => void, doNotificationList: (?Array<string>, ?boolean) => void,
doNotificationCategories: () => void, doNotificationCategories: () => void,
activeChannel: ?ChannelClaim, activeChannel: ?ChannelClaim,
doCommentReactList: (Array<string>) => Promise<any>, doCommentReactList: (Array<string>) => Promise<any>,
@ -97,7 +97,7 @@ export default function NotificationsPage(props: Props) {
try { try {
const matchingCategory = arrayNotificationCategories.find((category) => category.name === name); const matchingCategory = arrayNotificationCategories.find((category) => category.name === name);
if (matchingCategory) { if (matchingCategory) {
doNotificationList(matchingCategory.types); doNotificationList(matchingCategory.types, false);
} }
} catch (e) { } catch (e) {
console.error(e); // eslint-disable-line no-console console.error(e); // eslint-disable-line no-console