diff --git a/packages/web/.circleci/config.yml b/packages/web/.circleci/config.yml index 8af956c53e..28d8994a10 100644 --- a/packages/web/.circleci/config.yml +++ b/packages/web/.circleci/config.yml @@ -372,6 +372,7 @@ jobs: command: | mv build-production build cp ./resources/apple-app-site-association build + cp ./robots.txt build echo ${GA_ACCESS_TOKEN} | npx wrangler secret put GA_ACCESS_TOKEN --env production npx wrangler publish --env production diff --git a/packages/web/src/containers/deleted-page/DeletedPage.tsx b/packages/web/src/containers/deleted-page/DeletedPage.tsx index 852dbfc69e..a5c042487c 100644 --- a/packages/web/src/containers/deleted-page/DeletedPage.tsx +++ b/packages/web/src/containers/deleted-page/DeletedPage.tsx @@ -12,6 +12,7 @@ type DeletedPageContentProps = { canonicalUrl: string playable: Playable user: User + deletedByArtist?: boolean } const DeletedPage = ({ @@ -19,7 +20,8 @@ const DeletedPage = ({ description, canonicalUrl, playable, - user + user, + deletedByArtist = true }: DeletedPageContentProps) => { const isMobile = useIsMobile() @@ -34,6 +36,7 @@ const DeletedPage = ({ canonicalUrl={canonicalUrl} playable={playable} user={user} + deletedByArtist={deletedByArtist} > {content} diff --git a/packages/web/src/containers/deleted-page/DeletedPageProvider.tsx b/packages/web/src/containers/deleted-page/DeletedPageProvider.tsx index 39724cd667..a8de8f7b28 100644 --- a/packages/web/src/containers/deleted-page/DeletedPageProvider.tsx +++ b/packages/web/src/containers/deleted-page/DeletedPageProvider.tsx @@ -23,6 +23,7 @@ type OwnProps = { canonicalUrl: string user: User playable: Playable + deletedByArtist: boolean children: | React.ComponentType @@ -39,6 +40,7 @@ const DeletedPageProvider = ({ description, canonicalUrl, user, + deletedByArtist = true, playable, children: Children, currentQueueItem, @@ -88,7 +90,8 @@ const DeletedPageProvider = ({ playable, user, goToArtistPage, - getLineupProps + getLineupProps, + deletedByArtist } return diff --git a/packages/web/src/containers/deleted-page/components/desktop/DeletedPage.tsx b/packages/web/src/containers/deleted-page/components/desktop/DeletedPage.tsx index 5fa48edd4b..2564fe0835 100644 --- a/packages/web/src/containers/deleted-page/components/desktop/DeletedPage.tsx +++ b/packages/web/src/containers/deleted-page/components/desktop/DeletedPage.tsx @@ -18,7 +18,8 @@ import Playable from 'models/Playable' import { NestedNonNullable } from 'utils/typeUtils' const messages = { - trackDeleted: 'Track [Deleted By Artist]', + trackDeleted: 'Track [Deleted]', + trackDeletedByArtist: 'Track [Deleted By Artist]', playlistDeleted: 'Playlist [Deleted by Artist]', albumDeleted: 'Album [Deleted By Artist]', checkOut: (name: string) => `Check out more by ${name}`, @@ -59,6 +60,7 @@ export type DeletedPageProps = { title: string description: string canonicalUrl: string + deletedByArtist: boolean playable: Playable user: User | null @@ -79,6 +81,7 @@ const DeletedPage = g( canonicalUrl, playable, user, + deletedByArtist = true, getLineupProps, goToArtistPage }) => { @@ -91,6 +94,8 @@ const DeletedPage = g( ? isAlbum ? messages.albumDeleted : messages.playlistDeleted + : deletedByArtist + ? messages.trackDeletedByArtist : messages.trackDeleted const renderTile = () => { diff --git a/packages/web/src/containers/deleted-page/components/mobile/DeletedPage.tsx b/packages/web/src/containers/deleted-page/components/mobile/DeletedPage.tsx index cc9154ce71..a60b68490f 100644 --- a/packages/web/src/containers/deleted-page/components/mobile/DeletedPage.tsx +++ b/packages/web/src/containers/deleted-page/components/mobile/DeletedPage.tsx @@ -15,7 +15,8 @@ import Playable from 'models/Playable' import { NestedNonNullable } from 'utils/typeUtils' const messages = { - trackDeleted: 'Track [Deleted By Artist]', + trackDeleted: 'Track [Deleted]', + trackDeletedByArtist: 'Track [Deleted By Artist]', playlistDeleted: 'Playlist [Deleted by Artist]', albumDeleted: 'Album [Deleted By Artist]', checkOut: (name: string) => `Check out more by ${name}`, @@ -56,6 +57,7 @@ export type DeletedPageProps = { title: string description: string canonicalUrl: string + deletedByArtist: boolean playable: Playable user: User | null @@ -75,6 +77,7 @@ const DeletedPage = g( description, canonicalUrl, playable, + deletedByArtist = true, user, getLineupProps, goToArtistPage @@ -88,6 +91,8 @@ const DeletedPage = g( ? isAlbum ? messages.albumDeleted : messages.playlistDeleted + : deletedByArtist + ? messages.trackDeletedByArtist : messages.trackDeleted const renderTile = () => { diff --git a/packages/web/src/containers/track-page/TrackPageProvider.tsx b/packages/web/src/containers/track-page/TrackPageProvider.tsx index a2865a6d2d..e12dad3570 100644 --- a/packages/web/src/containers/track-page/TrackPageProvider.tsx +++ b/packages/web/src/containers/track-page/TrackPageProvider.tsx @@ -424,6 +424,7 @@ class TrackPageProvider extends Component< canonicalUrl={canonicalUrl} playable={{ metadata: track, type: PlayableType.TRACK }} user={user} + deletedByArtist={!track._blocked} /> ) } diff --git a/packages/web/src/models/Track.ts b/packages/web/src/models/Track.ts index 820ea89826..1f966525d2 100644 --- a/packages/web/src/models/Track.ts +++ b/packages/web/src/models/Track.ts @@ -116,6 +116,8 @@ export type ComputedTrackProperties = { _remix_parents?: Array<{ track_id: ID }> // Present iff the track has been cosigned _co_sign?: Nullable + + _blocked?: boolean } export type Track = TrackMetadata & ComputedTrackProperties diff --git a/packages/web/src/services/AudiusBackend.js b/packages/web/src/services/AudiusBackend.js index 010faae0bd..ac20b53220 100644 --- a/packages/web/src/services/AudiusBackend.js +++ b/packages/web/src/services/AudiusBackend.js @@ -22,6 +22,7 @@ import { import * as DiscoveryAPI from '@audius/libs/src/services/discoveryProvider/requests' import * as IdentityAPI from '@audius/libs/src/services/identity/requests' import { Timer } from 'utils/performance' +import { waitForRemoteConfig } from './remote-config/Provider' export const IDENTITY_SERVICE = process.env.REACT_APP_IDENTITY_SERVICE export const USER_NODE = process.env.REACT_APP_USER_NODE @@ -56,6 +57,18 @@ export const AuthHeaders = Object.freeze({ Signature: 'Encoded-Data-Signature' }) +export const waitForWeb3 = async () => { + if (!window.web3Loaded) { + await new Promise(resolve => { + const onLoad = () => { + window.removeEventListener('WEB3_LOADED', onLoad) + resolve() + } + window.addEventListener('WEB3_LOADED', onLoad) + }) + } +} + let AudiusLibs = null let Utils = null let SanityChecks = null @@ -303,25 +316,9 @@ class AudiusBackend { static async setup() { // Wait for web3 to load if necessary - if (!window.web3Loaded) { - await new Promise(resolve => { - const onLoad = () => { - window.removeEventListener('WEB3_LOADED', onLoad) - resolve() - } - window.addEventListener('WEB3_LOADED', onLoad) - }) - } + await waitForWeb3() // Wait for optimizely to load if necessary - if (!window.optimizelyDatafile) { - await new Promise(resolve => { - const onLoad = () => { - window.removeEventListener('OPTIMIZELY_LOADED', onLoad) - resolve() - } - window.addEventListener('OPTIMIZELY_LOADED', onLoad) - }) - } + await waitForRemoteConfig() const { libs, libsUtils, libsSanityChecks } = await import( './audius-backend/AudiusLibsLazyLoader' diff --git a/packages/web/src/services/remote-config/Provider.ts b/packages/web/src/services/remote-config/Provider.ts index 44fa0e3c6d..cbff4d9bfa 100644 --- a/packages/web/src/services/remote-config/Provider.ts +++ b/packages/web/src/services/remote-config/Provider.ts @@ -132,10 +132,7 @@ export function getFeatureEnabled(flag: FeatureFlags) { } } -// Internal - -const init = async () => { - console.time('remote-config') +export const waitForRemoteConfig = async () => { // Wait for optimizely to load if necessary (as it can be an async or defer tag) // @ts-ignore: injected in index.html if (!window.optimizelyDatafile) { @@ -146,6 +143,13 @@ const init = async () => { }) if (cb) window.removeEventListener('OPTIMIZELY_LOADED', cb) } +} + +// Internal + +const init = async () => { + console.time('remote-config') + await waitForRemoteConfig() provider = optimizely.createInstance({ // @ts-ignore: injected in index.html diff --git a/packages/web/src/services/remote-config/RemoteConfig.ts b/packages/web/src/services/remote-config/RemoteConfig.ts index 218c422b19..6c886b05b2 100644 --- a/packages/web/src/services/remote-config/RemoteConfig.ts +++ b/packages/web/src/services/remote-config/RemoteConfig.ts @@ -52,7 +52,12 @@ export enum StringKeys { /** * Custom eth provider urls to use for talking to main-net contracts */ - ETH_PROVIDER_URLS = 'ETH_PROVIDER_URLS' + ETH_PROVIDER_URLS = 'ETH_PROVIDER_URLS', + + /** + * Blocks content + */ + CONTENT_BLOCK_LIST = 'CONTENT_BLOCK_LIST' } export type AllRemoteConfigKeys = diff --git a/packages/web/src/services/remote-config/defaults.ts b/packages/web/src/services/remote-config/defaults.ts index 167d377a3e..b0bd8b8014 100644 --- a/packages/web/src/services/remote-config/defaults.ts +++ b/packages/web/src/services/remote-config/defaults.ts @@ -16,7 +16,8 @@ export const remoteConfigStringDefaults: { [StringKeys.AUDIUS_LOGO_VARIANT]: null, [StringKeys.AUDIUS_LOGO_VARIANT_CLICK_TARGET]: null, [StringKeys.APP_WIDE_NOTICE_TEXT]: null, - [StringKeys.ETH_PROVIDER_URLS]: ETH_PROVIDER_URLS + [StringKeys.ETH_PROVIDER_URLS]: ETH_PROVIDER_URLS, + [StringKeys.CONTENT_BLOCK_LIST]: null } export const remoteConfigDoubleDefaults: { [key in DoubleKeys]: number | null diff --git a/packages/web/src/store/cache/tracks/utils/blocklist.ts b/packages/web/src/store/cache/tracks/utils/blocklist.ts new file mode 100644 index 0000000000..0d92e2d50b --- /dev/null +++ b/packages/web/src/store/cache/tracks/utils/blocklist.ts @@ -0,0 +1,44 @@ +import { getRemoteVar, StringKeys } from 'services/remote-config' +import { TrackMetadata } from 'models/Track' +import { waitForRemoteConfig } from 'services/remote-config/Provider' +import { waitForWeb3 } from 'services/AudiusBackend' + +declare global { + interface Window { + Web3: any + } +} + +const IS_WEB_HOSTNAME = + window.location.hostname === process.env.REACT_APP_PUBLIC_HOSTNAME + +let blockList: Set + +const setBlocked = async (track: T) => { + // Initialize the set if not present + if (!blockList) { + await waitForRemoteConfig() + blockList = new Set( + (getRemoteVar(StringKeys.CONTENT_BLOCK_LIST) || '').split(',') + ) + } + if (IS_WEB_HOSTNAME) { + await waitForWeb3() + const shaId = window.Web3.utils.sha3(track.track_id.toString()) + if (blockList.has(shaId)) { + return { + ...track, + is_delete: true, + _blocked: true + } + } + } + // Most of the time this method is a no-op + return track +} + +export const setTracksIsBlocked = async ( + tracks: T[] +): Promise => { + return await Promise.all(tracks.map(setBlocked)) +} diff --git a/packages/web/src/store/cache/tracks/utils/processAndCacheTracks.ts b/packages/web/src/store/cache/tracks/utils/processAndCacheTracks.ts index 600eeb8232..07c4547a8c 100644 --- a/packages/web/src/store/cache/tracks/utils/processAndCacheTracks.ts +++ b/packages/web/src/store/cache/tracks/utils/processAndCacheTracks.ts @@ -1,10 +1,11 @@ -import { put } from 'redux-saga/effects' +import { put, call } from 'redux-saga/effects' import * as cacheActions from 'store/cache/actions' import { reformat } from './reformat' import { Kind } from 'store/types' import { makeUid } from 'utils/uid' import { addUsersFromTracks } from './helpers' import Track, { TrackMetadata } from 'models/Track' +import { setTracksIsBlocked } from './blocklist' /** * Processes tracks, adding users and calling `reformat`, before @@ -17,8 +18,10 @@ export function* processAndCacheTracks( // Add users yield addUsersFromTracks(tracks) + const checkedTracks: T[] = yield call(setTracksIsBlocked, tracks) + // Remove users, add images - const reformattedTracks = tracks.map(reformat) + const reformattedTracks = checkedTracks.map(reformat) // insert tracks into cache yield put( diff --git a/packages/web/src/store/cache/tracks/utils/retrieveTracks.ts b/packages/web/src/store/cache/tracks/utils/retrieveTracks.ts index 1ed967d3a6..e45045a393 100644 --- a/packages/web/src/store/cache/tracks/utils/retrieveTracks.ts +++ b/packages/web/src/store/cache/tracks/utils/retrieveTracks.ts @@ -15,6 +15,7 @@ import { import { fetchAndProcessStems } from './fetchAndProcessStems' import apiClient from 'services/audius-api-client/AudiusAPIClient' import { getUserId } from 'store/account/selectors' +import { setTracksIsBlocked } from './blocklist' type UnlistedTrackRequest = { id: ID; url_title: string; handle: string } type RetrieveTracksArgs = { @@ -150,7 +151,8 @@ export function* retrieveTracks({ deleteExistingEntry: false, onBeforeAddToCache: function* (tracks: T[]) { yield addUsersFromTracks(tracks) - return tracks.map(track => reformat(track)) + const checkedTracks = yield call(setTracksIsBlocked, tracks) + return checkedTracks.map(reformat) } }) diff --git a/packages/web/src/store/reachability/sagas.ts b/packages/web/src/store/reachability/sagas.ts index 98243038bf..9806d65575 100644 --- a/packages/web/src/store/reachability/sagas.ts +++ b/packages/web/src/store/reachability/sagas.ts @@ -15,8 +15,7 @@ const REACHABILITY_SHORT_TIMEOUT = 5 * 1000 // 5s const REACHABILITY_REQUEST_TIMEOUT = 15 * 1000 // 15s // Check that a response from REACHABILITY_URL is valid -const isResponseValid = (response: Response) => - response && response.ok && response.status === 204 +const isResponseValid = (response: Response) => response && response.ok function* ping() { // If there's no reachability url available, consider ourselves reachable