Skip to content

Commit 9d67ad0

Browse files
authored
Feat/474 - Project overview socials share (#272)
* feat(overview-social-share): added socials share service * feat(overview-social-share): moved social urls to separate config file
1 parent 5bff48c commit 9d67ad0

File tree

14 files changed

+242
-116
lines changed

14 files changed

+242
-116
lines changed

src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.html

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,36 @@
2222
</div>
2323

2424
<div class="flex flex-row justify-content-start gap-1">
25-
<a class="social-link flex align-items-center justify-content-center" [href]="emailShareLink()">
25+
<a
26+
class="social-link flex align-items-center justify-content-center"
27+
[href]="emailShareLink()"
28+
target="_blank"
29+
rel="noopener noreferrer"
30+
>
2631
<osf-icon [iconClass]="`fas fa-envelope fa-xl`" />
2732
</a>
28-
<a class="social-link flex align-items-center justify-content-center" [href]="twitterShareLink()">
33+
<a
34+
class="social-link flex align-items-center justify-content-center"
35+
[href]="twitterShareLink()"
36+
target="_blank"
37+
rel="noopener noreferrer"
38+
>
2939
<osf-icon [iconClass]="`fab fa-x-twitter fa-xl`" />
3040
</a>
31-
<a class="social-link flex align-items-center justify-content-center" [href]="facebookShareLink()">
41+
<a
42+
class="social-link flex align-items-center justify-content-center"
43+
[href]="facebookShareLink()"
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
>
3247
<osf-icon [iconClass]="`fab fa-facebook-f fa-xl`" />
3348
</a>
34-
<a class="social-link flex align-items-center justify-content-center" [href]="linkedInShareLink()">
49+
<a
50+
class="social-link flex align-items-center justify-content-center"
51+
[href]="linkedInShareLink()"
52+
target="_blank"
53+
rel="noopener noreferrer"
54+
>
3555
<osf-icon [iconClass]="`fab fa-linkedin-in fa-xl`" />
3656
</a>
3757
</div>

src/app/features/preprints/components/preprint-details/share-and-downlaod/share-and-download.component.ts

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { ButtonDirective } from 'primeng/button';
66
import { Card } from 'primeng/card';
77
import { Skeleton } from 'primeng/skeleton';
88

9-
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
9+
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
1010

1111
import { PreprintProviderDetails } from '@osf/features/preprints/models';
1212
import { PreprintSelectors } from '@osf/features/preprints/store/preprint';
1313
import { IconComponent } from '@shared/components';
14-
15-
import { environment } from 'src/environments/environment';
14+
import { ShareableContent } from '@shared/models';
15+
import { SocialShareService } from '@shared/services';
1616

1717
@Component({
1818
selector: 'osf-preprint-share-and-download',
@@ -24,6 +24,8 @@ import { environment } from 'src/environments/environment';
2424
export class ShareAndDownloadComponent {
2525
preprintProvider = input.required<PreprintProviderDetails | undefined>();
2626

27+
private readonly socialShareService = inject(SocialShareService);
28+
2729
preprint = select(PreprintSelectors.getPreprint);
2830
isPreprintLoading = select(PreprintSelectors.isPreprintLoading);
2931

@@ -40,65 +42,33 @@ export class ShareAndDownloadComponent {
4042

4143
if (!preprint) return '#';
4244

43-
return `${environment.webUrl}/download/${this.preprint()?.id}`;
45+
return this.socialShareService.createDownloadUrl(preprint.id);
4446
});
4547

46-
private preprintDetailsFullUrl = computed(() => {
48+
private shareableContent = computed((): ShareableContent | null => {
4749
const preprint = this.preprint();
4850
const preprintProvider = this.preprintProvider();
4951

50-
if (!preprint || !preprintProvider) return '';
52+
if (!preprint || !preprintProvider) return null;
5153

52-
return `${environment.webUrl}/preprints/${preprintProvider.id}/${preprint.id}`;
54+
return {
55+
id: preprint.id,
56+
title: preprint.title,
57+
description: preprint.description,
58+
url: this.socialShareService.createPreprintUrl(preprint.id, preprintProvider.id),
59+
};
5360
});
5461

55-
emailShareLink = computed(() => {
56-
const preprint = this.preprint();
57-
const preprintProvider = this.preprintProvider();
62+
shareLinks = computed(() => {
63+
const content = this.shareableContent();
5864

59-
if (!preprint || !preprintProvider) return;
65+
if (!content) return null;
6066

61-
const subject = encodeURIComponent(preprint.title);
62-
const body = encodeURIComponent(this.preprintDetailsFullUrl());
63-
64-
return `mailto:?subject=${subject}&body=${body}`;
67+
return this.socialShareService.generateAllSharingLinks(content);
6568
});
6669

67-
twitterShareLink = computed(() => {
68-
const preprint = this.preprint();
69-
const preprintProvider = this.preprintProvider();
70-
71-
if (!preprint || !preprintProvider) return '';
72-
73-
const url = encodeURIComponent(this.preprintDetailsFullUrl());
74-
const text = encodeURIComponent(preprint.title);
75-
76-
return `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
77-
});
78-
79-
facebookShareLink = computed(() => {
80-
const preprint = this.preprint();
81-
const preprintProvider = this.preprintProvider();
82-
83-
if (!preprint || !preprintProvider) return '';
84-
85-
const href = encodeURIComponent(this.preprintDetailsFullUrl());
86-
87-
const facebookAppId = preprintProvider.facebookAppId || environment.facebookAppId;
88-
return `https://www.facebook.com/dialog/share?app_id=${facebookAppId}&display=popup&href=${href}`;
89-
});
90-
91-
linkedInShareLink = computed(() => {
92-
const preprint = this.preprint();
93-
const preprintProvider = this.preprintProvider();
94-
95-
if (!preprint || !preprintProvider) return '';
96-
97-
const url = encodeURIComponent(this.preprintDetailsFullUrl());
98-
const title = encodeURIComponent(preprint.title);
99-
const summary = encodeURIComponent(preprint.description || preprint.title);
100-
const source = encodeURIComponent('OSF');
101-
102-
return `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}&summary=${summary}&source=${source}`;
103-
});
70+
emailShareLink = computed(() => this.shareLinks()?.email || '');
71+
twitterShareLink = computed(() => this.shareLinks()?.twitter || '');
72+
facebookShareLink = computed(() => this.shareLinks()?.facebook || '');
73+
linkedInShareLink = computed(() => this.shareLinks()?.linkedIn || '');
10474
}

src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@
9191
[pTooltip]="'project.overview.tooltips.share' | translate"
9292
tooltipPosition="bottom"
9393
>
94-
<span class="font-bold">{{ socialsActionItems.length }}</span>
94+
<span class="font-bold">{{ socialsActionItems().length }}</span>
9595
<i class="fas fa-share-nodes text-2xl"></i>
96-
<p-menu appendTo="body" #socialsActionMenu [model]="socialsActionItems" popup>
96+
<p-menu appendTo="body" #socialsActionMenu [model]="socialsActionItems()" popup>
9797
<ng-template #item let-item>
98-
<a class="p-menu-item-link">
98+
<a class="p-menu-item-link" [href]="item.url" target="_blank" rel="noopener noreferrer">
9999
<div class="social-link flex align-items-center justify-content-center">
100100
<osf-icon [iconClass]="`${item.icon} fa-sm`"></osf-icon>
101101
</div>

src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ import { Tooltip } from 'primeng/tooltip';
1111
import { timer } from 'rxjs';
1212

1313
import { NgClass } from '@angular/common';
14-
import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, signal } from '@angular/core';
14+
import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, inject, input, signal } from '@angular/core';
1515
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1616
import { FormsModule } from '@angular/forms';
1717
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
1818

1919
import { DuplicateDialogComponent, TogglePublicityDialogComponent } from '@osf/features/project/overview/components';
20+
import { SocialsShareActionItem } from '@osf/features/project/overview/models';
2021
import { IconComponent } from '@osf/shared/components';
21-
import { ToastService } from '@osf/shared/services';
22+
import { SocialShareService, ToastService } from '@osf/shared/services';
2223
import { ResourceType } from '@shared/enums';
23-
import { ToolbarResource } from '@shared/models';
24+
import { ShareableContent, ToolbarResource } from '@shared/models';
2425
import { FileSizePipe } from '@shared/pipes';
2526
import {
2627
AddResourceToBookmarks,
@@ -30,7 +31,6 @@ import {
3031
RemoveResourceFromBookmarks,
3132
} from '@shared/stores';
3233

33-
import { SOCIAL_ACTION_ITEMS } from '../../constants';
3434
import { ForkDialogComponent } from '../fork-dialog/fork-dialog.component';
3535

3636
@Component({
@@ -56,22 +56,28 @@ export class OverviewToolbarComponent {
5656
private dialogService = inject(DialogService);
5757
private translateService = inject(TranslateService);
5858
private toastService = inject(ToastService);
59+
private socialShareService = inject(SocialShareService);
5960
protected destroyRef = inject(DestroyRef);
6061
private readonly router = inject(Router);
6162
private readonly route = inject(ActivatedRoute);
6263
protected isPublic = signal(false);
6364
protected isBookmarked = signal(false);
64-
6565
isCollectionsRoute = input<boolean>(false);
6666
isAdmin = input.required<boolean>();
6767
currentResource = input.required<ToolbarResource | null>();
68+
projectTitle = input<string>('');
69+
projectDescription = input<string>('');
6870
showViewOnlyLinks = input<boolean>(true);
6971

7072
protected isBookmarksLoading = select(MyResourcesSelectors.getBookmarksLoading);
7173
protected isBookmarksSubmitting = select(BookmarksSelectors.getBookmarksCollectionIdSubmitting);
7274
protected bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId);
7375
protected bookmarkedProjects = select(MyResourcesSelectors.getBookmarks);
74-
protected readonly socialsActionItems = SOCIAL_ACTION_ITEMS;
76+
protected socialsActionItems = computed(() => {
77+
const shareableContent = this.createShareableContent();
78+
return shareableContent ? this.buildSocialActionItems(shareableContent) : [];
79+
});
80+
7581
protected readonly forkActionItems = [
7682
{
7783
label: 'project.overview.actions.forkProject',
@@ -122,38 +128,6 @@ export class OverviewToolbarComponent {
122128
});
123129
}
124130

125-
private handleForkResource(): void {
126-
const resource = this.currentResource();
127-
const headerTranslation =
128-
resource?.resourceType === ResourceType.Project
129-
? 'project.overview.dialog.fork.headerProject'
130-
: resource?.resourceType === ResourceType.Registration
131-
? 'project.overview.dialog.fork.headerRegistry'
132-
: '';
133-
if (resource) {
134-
this.dialogService.open(ForkDialogComponent, {
135-
focusOnShow: false,
136-
header: this.translateService.instant(headerTranslation),
137-
closeOnEscape: true,
138-
modal: true,
139-
closable: true,
140-
data: {
141-
resource: resource,
142-
},
143-
});
144-
}
145-
}
146-
147-
private handleDuplicateProject(): void {
148-
this.dialogService.open(DuplicateDialogComponent, {
149-
focusOnShow: false,
150-
header: this.translateService.instant('project.overview.dialog.duplicate.header'),
151-
closeOnEscape: true,
152-
modal: true,
153-
closable: true,
154-
});
155-
}
156-
157131
protected handleToggleProjectPublicity(): void {
158132
const resource = this.currentResource();
159133
if (!resource) return;
@@ -212,4 +186,91 @@ export class OverviewToolbarComponent {
212186
});
213187
}
214188
}
189+
190+
private handleForkResource(): void {
191+
const resource = this.currentResource();
192+
const headerTranslation =
193+
resource?.resourceType === ResourceType.Project
194+
? 'project.overview.dialog.fork.headerProject'
195+
: resource?.resourceType === ResourceType.Registration
196+
? 'project.overview.dialog.fork.headerRegistry'
197+
: '';
198+
if (resource) {
199+
this.dialogService.open(ForkDialogComponent, {
200+
focusOnShow: false,
201+
header: this.translateService.instant(headerTranslation),
202+
closeOnEscape: true,
203+
modal: true,
204+
closable: true,
205+
data: {
206+
resource: resource,
207+
},
208+
});
209+
}
210+
}
211+
212+
private handleDuplicateProject(): void {
213+
this.dialogService.open(DuplicateDialogComponent, {
214+
focusOnShow: false,
215+
header: this.translateService.instant('project.overview.dialog.duplicate.header'),
216+
closeOnEscape: true,
217+
modal: true,
218+
closable: true,
219+
});
220+
}
221+
222+
private createShareableContent(): ShareableContent | null {
223+
const resource = this.currentResource();
224+
const title = this.projectTitle();
225+
const description = this.projectDescription();
226+
227+
if (!resource?.isPublic || !title) {
228+
return null;
229+
}
230+
231+
return {
232+
id: resource.id,
233+
title,
234+
description,
235+
url: this.buildResourceUrl(resource),
236+
};
237+
}
238+
239+
private buildResourceUrl(resource: ToolbarResource): string {
240+
switch (resource.resourceType) {
241+
case ResourceType.Project:
242+
return this.socialShareService.createProjectUrl(resource.id);
243+
case ResourceType.Registration:
244+
return this.socialShareService.createRegistrationUrl(resource.id);
245+
default:
246+
return `${window.location.origin}/${resource.id}`;
247+
}
248+
}
249+
250+
private buildSocialActionItems(shareableContent: ShareableContent): SocialsShareActionItem[] {
251+
const shareLinks = this.socialShareService.generateAllSharingLinks(shareableContent);
252+
253+
return [
254+
{
255+
label: 'project.overview.actions.socials.email',
256+
icon: 'fas fa-envelope',
257+
url: shareLinks.email,
258+
},
259+
{
260+
label: 'project.overview.actions.socials.x',
261+
icon: 'fab fa-x-twitter',
262+
url: shareLinks.twitter,
263+
},
264+
{
265+
label: 'project.overview.actions.socials.linkedIn',
266+
icon: 'fab fa-linkedin',
267+
url: shareLinks.linkedIn,
268+
},
269+
{
270+
label: 'project.overview.actions.socials.facebook',
271+
icon: 'fab fa-facebook-f',
272+
url: shareLinks.facebook,
273+
},
274+
];
275+
}
215276
}

src/app/features/project/overview/constants/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/app/features/project/overview/constants/social-actions.constants.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './project-overview.models';
2+
export * from './socials-share-action-item.model';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface SocialsShareActionItem {
2+
label: string;
3+
icon: string;
4+
url: string;
5+
}

src/app/features/project/overview/project-overview.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<osf-overview-toolbar
1717
[isCollectionsRoute]="isCollectionsRoute()"
1818
[currentResource]="currentResource()"
19+
[projectTitle]="project.title"
20+
[projectDescription]="project.description"
1921
[isAdmin]="isAdmin"
2022
/>
2123
</div>

0 commit comments

Comments
 (0)