Skip to content

Commit 38a7e46

Browse files
authored
[ENG-9731] Create a "tombstone" page for content that is flagged as spam (#899)
- Ticket: https://openscience.atlassian.net/browse/ENG-9731 - Feature flag: n/a ## Purpose Create a "tombstone" page for content that is flagged as spam Problem Statement When viewing spammed content (nodes, preprints) the URL associated with the content should be static and the page should display a notice that if this content should be available to reach out to [support@osf.io](mailto:support@osf.io) , this would re-assure users that their content had not been deleted or removed, and notify them to reach out to support for assistance. Solution Implement a static “flagged content” page for any spam-flagged item. When accessed, the page should include the microcopy below. See the mockup for details
1 parent 5a3adbf commit 38a7e46

9 files changed

Lines changed: 103 additions & 5 deletions

File tree

src/app/app.routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ export const routes: Routes = [
175175
import('./core/components/page-not-found/page-not-found.component').then((mod) => mod.PageNotFoundComponent),
176176
data: { skipBreadcrumbs: true },
177177
},
178+
{
179+
path: 'spam-content',
180+
loadComponent: () =>
181+
import('./core/components/resource-is-spammed/resource-is-spammed.component').then(
182+
(mod) => mod.ResourceIsSpammedComponent
183+
),
184+
data: { skipBreadcrumbs: true },
185+
},
178186
{
179187
path: 'project/:id/node/:nodeId/files/:provider/:fileId',
180188
loadComponent: () =>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<section class="text-center flex flex-column flex-1 my-7 mx-3 p-3 md:p-4">
2+
<h2>{{ 'resourceSpammed.title' | translate }}</h2>
3+
4+
<p class="my-4">
5+
{{ 'resourceSpammed.message' | translate }}
6+
</p>
7+
8+
<p>
9+
<span>{{ 'resourceSpammed.contact' | translate }}</span>
10+
<a class="ml-1 font-bold" [href]="'mailto:' + supportEmail">{{ supportEmail }} </a>
11+
<span>{{ 'resourceSpammed.footer' | translate }}</span>
12+
</p>
13+
</section>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@use "styles/mixins" as mix;
2+
3+
:host {
4+
@include mix.flex-center;
5+
flex: 1;
6+
background: var(--gradient-3);
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { ResourceIsSpammedComponent } from './resource-is-spammed.component';
4+
5+
import { provideOSFCore } from '@testing/osf.testing.provider';
6+
7+
describe('ResourceIsSpammedComponent', () => {
8+
let component: ResourceIsSpammedComponent;
9+
let fixture: ComponentFixture<ResourceIsSpammedComponent>;
10+
11+
beforeEach(() => {
12+
TestBed.configureTestingModule({
13+
imports: [ResourceIsSpammedComponent],
14+
providers: [provideOSFCore()],
15+
});
16+
17+
fixture = TestBed.createComponent(ResourceIsSpammedComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
26+
it('should set supportEmail from environment token', () => {
27+
expect(component.supportEmail).toBe('support@test.com');
28+
});
29+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TranslatePipe } from '@ngx-translate/core';
2+
3+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
4+
5+
import { ENVIRONMENT } from '@core/provider/environment.provider';
6+
7+
@Component({
8+
selector: 'osf-resource-is-spammed',
9+
imports: [TranslatePipe],
10+
templateUrl: './resource-is-spammed.component.html',
11+
styleUrl: './resource-is-spammed.component.scss',
12+
changeDetection: ChangeDetectionStrategy.OnPush,
13+
})
14+
export class ResourceIsSpammedComponent {
15+
private readonly environment = inject(ENVIRONMENT);
16+
readonly supportEmail = this.environment.supportEmail;
17+
}

src/app/features/preprints/services/preprints.service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { map, Observable } from 'rxjs';
1+
import { catchError, map, Observable, throwError } from 'rxjs';
22

33
import { inject, Injectable } from '@angular/core';
4+
import { Router } from '@angular/router';
45

56
import { ENVIRONMENT } from '@core/provider/environment.provider';
67
import { RegistryModerationMapper } from '@osf/features/moderation/mappers';
@@ -38,6 +39,7 @@ import {
3839
export class PreprintsService {
3940
private readonly jsonApiService = inject(JsonApiService);
4041
private readonly environment = inject(ENVIRONMENT);
42+
private readonly router = inject(Router);
4143

4244
get apiUrl() {
4345
return `${this.environment.apiDomainUrl}/v2`;
@@ -95,7 +97,15 @@ export class PreprintsService {
9597
null
9698
>
9799
>(`${this.apiUrl}/preprints/${id}/`, params)
98-
.pipe(map((response) => PreprintsMapper.fromPreprintWithEmbedsJsonApi(response)));
100+
.pipe(
101+
map((response) => PreprintsMapper.fromPreprintWithEmbedsJsonApi(response)),
102+
catchError((error) => {
103+
if (error.status === 410) {
104+
this.router.navigate(['/spam-content']);
105+
}
106+
return throwError(() => error);
107+
})
108+
);
99109
}
100110

101111
getPreprintMetrics(id: string) {

src/app/shared/services/resource.service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { finalize, map, Observable } from 'rxjs';
1+
import { catchError, finalize, map, Observable, throwError } from 'rxjs';
22

33
import { inject, Injectable } from '@angular/core';
4+
import { Router } from '@angular/router';
45

56
import { ENVIRONMENT } from '@core/provider/environment.provider';
67

@@ -24,6 +25,7 @@ export class ResourceGuidService {
2425
private jsonApiService = inject(JsonApiService);
2526
private loaderService = inject(LoaderService);
2627
private readonly environment = inject(ENVIRONMENT);
28+
private readonly router = inject(Router);
2729

2830
get apiUrl() {
2931
return `${this.environment.apiDomainUrl}/v2`;
@@ -59,7 +61,13 @@ export class ResourceGuidService {
5961
title: res.data.attributes?.title,
6062
}) as CurrentResource
6163
),
62-
finalize(() => this.loaderService.hide())
64+
finalize(() => this.loaderService.hide()),
65+
catchError((error) => {
66+
if (error.status === 410) {
67+
this.router.navigate(['/spam-content']);
68+
}
69+
return throwError(() => error);
70+
})
6371
);
6472
}
6573

src/assets/i18n/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2831,6 +2831,12 @@
28312831
"requestedSuccessMessage": "Your request for access has been sent.",
28322832
"alreadyRequestedMessage": "You already requested access."
28332833
},
2834+
"resourceSpammed": {
2835+
"title": "This page is temporarily unavailable.",
2836+
"message": "This content was flagged as potential spam.",
2837+
"contact": "If this is a mistake, reach out to",
2838+
"footer": "for assistance. Your content remains safe and will be restored once verified."
2839+
},
28342840
"validation": {
28352841
"required": "The field is required.",
28362842
"email": "Please enter a valid email address.",

src/environments/environment.docker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const environment = {
22
production: false,
33
webUrl: 'http://localhost:5000',
44
apiDomainUrl: 'http://localhost:8000',
5-
shareTroveUrl: 'https://localhost:8003/trove',
5+
shareTroveUrl: 'http://localhost:8003/trove',
66
addonsApiUrl: 'http://localhost:8004/v1',
77
funderApiUrl: 'https://api.crossref.org/',
88
casUrl: 'http://localhost:8080',

0 commit comments

Comments
 (0)