diff --git a/angular.json b/angular.json index 3853f4431a..15e74b26cf 100644 --- a/angular.json +++ b/angular.json @@ -30,7 +30,7 @@ "node_modules/bootstrap/dist/css/bootstrap.css", "node_modules/academicons/css/academicons.css", "src/styles.scss", - "src/material.scss", + "src/material.scss" ], "scripts": [ "node_modules/jquery/dist/jquery.js", @@ -50,7 +50,7 @@ "node_modules/ace-builds/src-min-noconflict/mode-perl.js", "node_modules/ace-builds/src-min-noconflict/theme-idle_fingers.js", "node_modules/ace-builds/src-min-noconflict/ext-searchbox.js", - "node_modules/bootstrap/dist/js/bootstrap.js", + "node_modules/bootstrap/dist/js/bootstrap.js" ] }, "configurations": { diff --git a/cypress/e2e/group1/dashboard.ts b/cypress/e2e/group1/dashboard.ts index b7b01f9602..234d65e71b 100644 --- a/cypress/e2e/group1/dashboard.ts +++ b/cypress/e2e/group1/dashboard.ts @@ -20,6 +20,7 @@ import { verifyGithubLinkDashboard, checkFeaturedContent, checkNewsAndUpdates, + checkMastodonFeed, } from '../../support/commands'; describe('Dockstore dashboard', () => { @@ -67,6 +68,11 @@ describe('Dockstore dashboard', () => { cy.visit('/dashboard'); checkNewsAndUpdates(); }); + + it('mastodon feed should be visible', () => { + cy.visit('/dashboard'); + checkMastodonFeed(); + }); }); describe('should display added notebook correctly', () => { diff --git a/cypress/e2e/immutableDatabaseTests/app-spec.ts b/cypress/e2e/immutableDatabaseTests/app-spec.ts index 80d8ff1044..646c9eb37c 100644 --- a/cypress/e2e/immutableDatabaseTests/app-spec.ts +++ b/cypress/e2e/immutableDatabaseTests/app-spec.ts @@ -27,9 +27,9 @@ describe('Logged in Dockstore Home', () => { // expect(browser.getLocationAbsUrl()).toMatch("/"); }); - it('should have the twitter timeline', () => { + it('should have the mastodon timeline', () => { cy.scrollTo('bottom'); - cy.get('.twitter-timeline').should('be.visible'); + cy.get('[data-cy=mt-toot]').should('be.visible'); }); function starColumn(url: string, type: string) { @@ -65,9 +65,9 @@ describe('Logged out Dockstore Home', () => { cy.get('#youtubeModal').should('not.exist'); }); - it('should have the twitter timeline', () => { + it('should have the mastodon timeline', () => { cy.scrollTo('bottom'); - cy.get('.twitter-timeline').should('be.visible'); + cy.get('[data-cy=mt-toot]').should('be.visible'); }); }); }); diff --git a/cypress/e2e/smokeTests/sharedTests/basic-enduser.ts b/cypress/e2e/smokeTests/sharedTests/basic-enduser.ts index 78b79b5a45..94608878c1 100644 --- a/cypress/e2e/smokeTests/sharedTests/basic-enduser.ts +++ b/cypress/e2e/smokeTests/sharedTests/basic-enduser.ts @@ -1,6 +1,6 @@ import { ga4ghPath } from '../../../../src/app/shared/constants'; import { ToolDescriptor } from '../../../../src/app/shared/openapi'; -import { goToTab, checkFeaturedContent, checkNewsAndUpdates } from '../../../support/commands'; +import { goToTab, checkFeaturedContent, checkNewsAndUpdates, checkMastodonFeed } from '../../../support/commands'; // Test an entry, these should be ambiguous between tools, workflows, and notebooks. describe('run stochastic smoke test', () => { @@ -372,6 +372,11 @@ describe('Check extra content', () => { cy.visit('/'); checkNewsAndUpdates(); }); + + it('mastodon feed should be visible', () => { + cy.visit('/'); + checkMastodonFeed(); + }); }); // TODO: uncomment after tooltester logs are fixed diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 3b00d28736..81e5c9694d 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -277,3 +277,7 @@ export function checkNewsAndUpdates() { cy.get('.news-entry').first().contains('a').should('have.attr', 'href'); }); } + +export function checkMastodonFeed() { + cy.get('[data-cy=mt-toot]').should('exist'); +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5501ce2a65..1178b0fee3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -102,7 +102,6 @@ import { RefreshService } from './shared/refresh.service'; import { ApiModule } from './shared/openapi/api.module'; import { Configuration } from './shared/openapi/configuration'; import { TrackLoginService } from './shared/track-login.service'; -import { TwitterService } from './shared/twitter.service'; import { UrlResolverService } from './shared/url-resolver.service'; import { VerifiedByService } from './shared/verified-by.service'; import { SitemapComponent } from './sitemap/sitemap.component'; @@ -230,7 +229,6 @@ export function initializerFactory( RegisterCheckerWorkflowService, RefreshService, PagenumberService, - TwitterService, GA4GHV20Service, DescriptorLanguageService, UrlResolverService, diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 10a1035178..2018c009d7 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -196,6 +196,19 @@ > +
  • + + small Mastodon logo@dockstore + +
  • -
    - +
    +
    diff --git a/src/app/home-page/dashboard/dashboard.component.spec.ts b/src/app/home-page/dashboard/dashboard.component.spec.ts index 849a7a195d..d727c3accc 100644 --- a/src/app/home-page/dashboard/dashboard.component.spec.ts +++ b/src/app/home-page/dashboard/dashboard.component.spec.ts @@ -4,13 +4,13 @@ import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { RouterTestingModule } from '@angular/router/testing'; -import { TwitterService } from '../../shared/twitter.service'; import { DashboardComponent } from './dashboard.component'; import { RegisterToolService } from 'app/container/register-tool/register-tool.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { ContainerService } from '../../shared/container.service'; import { ContainerStubService } from '../../test/service-stubs'; +import { MastodonService } from '../../shared/mastodon/mastodon.service'; describe('DashboardComponent', () => { let component: DashboardComponent; @@ -22,7 +22,7 @@ describe('DashboardComponent', () => { declarations: [DashboardComponent], schemas: [NO_ERRORS_SCHEMA], imports: [RouterTestingModule, MatButtonModule, MatIconModule, MatDialogModule, HttpClientTestingModule, MatSnackBarModule], - providers: [TwitterService, RegisterToolService, { provide: ContainerService, useClass: ContainerStubService }], + providers: [MastodonService, RegisterToolService, { provide: ContainerService, useClass: ContainerStubService }], }).compileComponents(); }) ); diff --git a/src/app/home-page/dashboard/dashboard.component.ts b/src/app/home-page/dashboard/dashboard.component.ts index b2115c6d59..7a30a0c4bc 100644 --- a/src/app/home-page/dashboard/dashboard.component.ts +++ b/src/app/home-page/dashboard/dashboard.component.ts @@ -5,7 +5,6 @@ import { RegisterToolService } from 'app/container/register-tool/register-tool.s import { MatDialog } from '@angular/material/dialog'; import { RegisterToolComponent } from 'app/container/register-tool/register-tool.component'; import { AlertService } from 'app/shared/alert/state/alert.service'; -import { TwitterService } from 'app/shared/twitter.service'; import { Dockstore } from 'app/shared/dockstore.model'; import { EntryType } from '../../shared/openapi'; @@ -17,17 +16,11 @@ import { EntryType } from '../../shared/openapi'; export class DashboardComponent extends Base implements OnInit { public Dockstore = Dockstore; @ViewChild('twitter') twitterElement: ElementRef; - constructor( - private registerToolService: RegisterToolService, - private dialog: MatDialog, - private alertService: AlertService, - private twitterService: TwitterService - ) { + constructor(private registerToolService: RegisterToolService, private dialog: MatDialog, private alertService: AlertService) { super(); } ngOnInit() { - this.loadTwitterWidget(); this.registerToolService.isModalShown.pipe(takeUntil(this.ngUnsubscribe)).subscribe((isModalShown: boolean) => { if (isModalShown) { const dialogRef = this.dialog.open(RegisterToolComponent, { width: '500px' }); @@ -44,17 +37,5 @@ export class DashboardComponent extends Base implements OnInit { }); } - private loadTwitterWidget() { - this.twitterService - .loadScript() - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe( - () => { - this.twitterService.createTimeline(this.twitterElement, 2); - }, - (error) => console.error(error) - ); - } - protected readonly EntryType = EntryType; } diff --git a/src/app/home-page/home-logged-out/home.component.html b/src/app/home-page/home-logged-out/home.component.html index adce0980f4..5ee7dca98f 100644 --- a/src/app/home-page/home-logged-out/home.component.html +++ b/src/app/home-page/home-logged-out/home.component.html @@ -1,5 +1,5 @@ + + + + diff --git a/src/app/shared/mastodon/mastodon.component.scss b/src/app/shared/mastodon/mastodon.component.scss new file mode 100644 index 0000000000..758bec5108 --- /dev/null +++ b/src/app/shared/mastodon/mastodon.component.scss @@ -0,0 +1,395 @@ +/* Mastodon embed feed timeline v3.8.2 */ +/* More info at: */ +/* https://gitlab.com/idotj/mastodon-embed-feed-timeline */ + +/* Theme colors */ +:root, +html[data-theme='light'] { + --bg-color: #fff; + --bg-hover-color: #d9e1e8; + --line-gray-color: #c0cdd9; + --content-text: #000; + --link-color: #3a3bff; + --error-text-color: #8b0000; +} +html[data-theme='dark'] { + --bg-color: #282c37; + --bg-hover-color: #313543; + --line-gray-color: #393f4f; + --content-text: #fff; + --link-color: #8c8dff; + --error-text-color: #fe6c6c; +} + +/* Main container */ + +.hide { + display: none; +} +.mt-timeline { + display: flex; + height: 50rem; + position: relative; + background: var(--bg-color); +} + +.mt-timeline a:link, +.mt-timeline a:active, +.mt-timeline a { + text-decoration: none; + color: var(--link-color); +} + +.mt-timeline a:not(.toot-preview-link):hover { + text-decoration: underline; +} + +.mt-timeline::-webkit-scrollbar { + width: 0.75rem; + height: 0.75rem; +} + +.mt-timeline::-webkit-scrollbar-corner { + background: transparent; +} + +.mt-timeline::-webkit-scrollbar-thumb { + border: 0 var(--content-text); + border-radius: 2rem; + background: var(--bg-hover-color); +} + +.mt-timeline::-webkit-scrollbar-track { + border: 0 var(--content-text); + border-radius: 0; + background: rgba(0, 0, 0, 0.1); +} + +.mt-header-text { + color: #282c37; +} + +.mt-header-link { + text-decoration: underline !important; +} +.mt-body { + padding: 1rem 1.5rem; + white-space: pre-wrap; + word-wrap: break-word; + overflow-y: auto; + height: 100%; + scrollbar-color: var(--bg-hover-color) rgba(0, 0, 0, 0.1); +} + +.mt-body .invisible { + font-size: 0; + line-height: 0; + display: inline-block; + width: 0; + height: 0; + position: absolute; +} + +/* Toot container */ +.mt-toot { + margin: 0.25rem; + padding: 1rem 0.5rem 1.5rem 4rem; + position: relative; + min-height: 3.75rem; + background-color: transparent; + border-bottom: 1px solid lightgrey; +} + +.mt-toot:hover, +.mt-toot:focus { + cursor: pointer; + background-color: var(--bg-hover-color); +} + +.mt-toot p:last-child { + margin-bottom: 0; +} + +mat-card-content { + height: 80%; +} +/* User icon */ +.mt-avatar { + position: absolute; + top: 1rem; + left: 0.25rem; + width: 3rem; + height: 3rem; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: contain; + background-color: var(--bg-color); + border-radius: 0.25rem; +} + +.mt-avatar-boosted { + width: 2.5rem; + height: 2.5rem; +} + +.mt-avatar-booster { + width: 1.5rem; + height: 1.5rem; + top: 1.5rem; + left: 1.5rem; +} + +.mt-user { + display: table; + font-weight: 600; + margin-bottom: 1rem; +} + +.mt-user > a { + color: var(--content-text) !important; +} + +/* Text */ +.toot-text { + margin-bottom: 0.25rem; + color: var(--content-text); +} + +.toot-text .spoiler-link { + display: inline-block; +} + +.toot-text .spoiler-text-hidden { + display: none; +} + +.toot-text.truncate { + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: var(--text-max-lines); + -webkit-box-orient: vertical; +} + +.toot-text:not(.truncate) .ellipsis::after { + content: '...'; +} + +.toot-text blockquote { + border-left: 0.25rem solid var(--line-gray-color); + margin-left: 0; + padding-left: 0.5rem; +} + +.toot-text .custom-emoji { + height: 1.5rem; + min-width: 1.5rem; + margin-bottom: -0.25rem; + width: auto; +} + +.mt-error { + position: absolute; + display: flex; + flex-direction: column; + height: calc(100% - 3.5rem); + width: calc(100% - 4.5rem); + justify-content: center; + align-items: center; + color: var(--error-text-color); + padding: 0.75rem; + text-align: center; +} + +.mt-error-icon { + font-size: 2rem; +} + +.mt-error-message { + padding: 1rem 0; +} + +.mt-error-message hr { + color: var(--line-gray-color); +} + +/* Poll */ +.toot-poll { + margin-bottom: 0.25rem; + color: var(--content-text); +} + +.toot-poll ul { + list-style: none; + padding: 0; + margin: 0; +} + +.toot-poll ul li { + font-size: 0.9rem; + margin-bottom: 0.5rem; +} + +.toot-poll ul li:not(:last-child) { + margin-bottom: 0.25rem; +} + +.toot-poll ul li:before { + content: '◯'; + padding-right: 0.5rem; +} + +/* Medias */ +.toot-media { + overflow: hidden; + margin-bottom: 0.5rem; +} + +.toot-media-preview { + position: relative; + margin-top: 0.25rem; + height: auto; + text-align: center; + width: 100%; +} + +.toot-media > .spoiler-link { + position: absolute; + top: 50%; + left: 50%; + z-index: 1; + transform: translate(-50%, -50%); +} + +.toot-media-spoiler > img { + filter: blur(2rem); +} + +.toot-media-preview a { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.img-ratio14_7 { + position: relative; + padding-top: 56.95%; + width: 100%; +} + +.img-ratio14_7 > img { + width: 100%; + height: auto; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +/* Preview link */ +.toot-preview-link { + min-height: 4rem; + display: flex; + flex-direction: row; + + border: 1px solid var(--line-gray-color); + border-radius: 0.5rem; + color: var(--link-color); + font-size: 0.8rem; + margin: 1rem 0 0.5rem 0; + overflow: hidden; +} + +.toot-preview-image { + width: 40%; + align-self: stretch; +} + +.toot-preview-image img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.toot-preview-noImage { + width: 40%; + font-size: 1.5rem; + align-self: center; + text-align: center; +} + +.toot-preview-content { + width: 60%; + display: flex; + align-self: center; + flex-direction: column; + padding: 0.5rem 1rem; + gap: 0.5rem; +} + +.toot-preview-title { + font-weight: 600; +} + +/* Spoiler button */ +.spoiler-link { + border-radius: 2px; + background-color: var(--line-gray-color); + border: 0; + color: var(--content-text); + font-weight: 700; + font-size: 0.7rem; + padding: 0 0.35rem; + text-transform: uppercase; + line-height: 1.25rem; + cursor: pointer; + vertical-align: top; +} + +/* Date */ +.toot-date { + font-size: 0.75rem; +} + +/* Loading-spinner */ +.mt-body > .loading-spinner { + position: absolute; + width: 3rem; + height: 3rem; + margin: auto; + top: calc(50% - 1.5rem); + right: calc(50% - 1.5rem); +} + +.loading-spinner { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.0' viewBox='0 0 128 128' %3E%3Cg%3E%3Cpath d='M64 128A64 64 0 0 1 18.34 19.16L21.16 22a60 60 0 1 0 52.8-17.17l.62-3.95A64 64 0 0 1 64 128z' fill='%23404040'/%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'%3E%3C/animateTransform%3E%3C/g%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; + background-size: min(2.5rem, calc(100% - 0.5rem)); +} + +/* Footer (See more link) */ +.mt-footer { + margin: 1rem auto 2rem auto; + padding: 0 2rem; + text-align: center; + color: #3a3bff; +} + +/* Hidden element */ +.visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} diff --git a/src/app/shared/mastodon/mastodon.component.ts b/src/app/shared/mastodon/mastodon.component.ts new file mode 100644 index 0000000000..81645bdc66 --- /dev/null +++ b/src/app/shared/mastodon/mastodon.component.ts @@ -0,0 +1,152 @@ +/** + * Mastodon embed feed timeline v3.8.2 + * More info at: + * https://gitlab.com/idotj/mastodon-embed-feed-timeline + */ + +import { Component } from '@angular/core'; +import { MastodonService } from './mastodon.service'; // Import the service + +export interface MastodonTimelineData { + postUrl: string; + accountUrl: string; + accountAvatar: string; + reblogAccountAvatar: string; + accountUsername: string; + postDate: string; + postContent: string; + spoilerText: string; + mediaContent: any[]; + previewLink: string; + poll: any[]; +} + +@Component({ + selector: 'app-mastodon-timeline', + templateUrl: './mastodon.component.html', + styleUrls: ['./mastodon.component.scss'], +}) +export class MastodonComponent { + fetchedData: Map; + timelineData: MastodonTimelineData[] = []; + containerBodyId: string = 'mt-body'; // Id of the
    containing the timeline + defaultTheme: string = 'light'; // Preferred color theme: 'light', 'dark' or 'auto'. Default: auto + instanceUrl: string = 'https://genomic.social'; // Your Mastodon instance + timelineType: string = 'profile'; // Choose type of toots to show in the timeline: 'local', 'profile', 'hashtag'. Default: local + userId: string = '110973634882132620'; // Your user ID on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline + profileName: string = 'dockstore'; // Your user name on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline + hashtagName: string = ''; // The name of the hashtag. Leave empty if you didn't choose 'hashtag' as type of timeline + tootsLimit: string = '5'; // Maximum amount of toots to get. Default: 20 + hideUnlisted: boolean = false; // Hide unlisted toots. Default: don't hide + hideReblog: boolean = false; // Hide boosted toots. Default: don't hide + hideReplies: boolean = false; // Hide replies toots. Default: don't hide + hidePreviewLink: boolean = false; // Hide preview card if toot contains a link, photo or video from a URL. Default: don't hide + hideEmojis: boolean = false; // Hide custom emojis available on the server. Default: don't hide + markdownBlockquote: boolean = false; // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag. Ddefault: don't apply + textMaxLines: string = '0'; // Limit the text content to a maximum number of lines. Default: 0 (unlimited) + linkSeeMore: string = 'See more posts at Mastodon'; // Customize the text of the link pointing to the Mastodon page (appears after the last toot) + mastodonDockstoreLink = this.instanceUrl + '/@' + this.profileName; + + constructor(private mastodonService: MastodonService) {} + + ngOnInit(): void { + // Initialize the Mastodon API and build the timeline here + const params = { + containerBodyId: this.containerBodyId, + defaultTheme: this.defaultTheme, + instanceUrl: this.instanceUrl, + timelineType: this.timelineType, + userId: this.userId, + profileName: this.profileName, + hashtagName: this.hashtagName, + tootsLimit: this.tootsLimit, + hideUnlisted: this.hideUnlisted, + hideReblog: this.hideReblog, + hideReplies: this.hideReplies, + hidePreviewLink: this.hidePreviewLink, + hideEmojis: this.hideEmojis, + markdownBlockquote: this.markdownBlockquote, + textMaxLines: this.textMaxLines, + linkSeeMore: this.linkSeeMore, + }; + this.mastodonService.initialize(params); + this.mastodonService.fetchedDataSubject.subscribe((data) => { + this.fetchedData = data; + const timeline = this.fetchedData['timeline']; + let postUrl; + let accountUrl; + let accountAvatar; + let reblogAccountAvatar; + let accountUsername; + let postDate; + let postContent; + let spoilerText; + let mediaContent; + let previewLink; + let poll; + for (let i = 0; i < timeline.length; i++) { + if (timeline[i]['reblog']) { + postUrl = timeline[i]['reblog']['url']; + accountUrl = timeline[i]['reblog']['account']['url']; + accountAvatar = timeline[i]['account']['avatar']; + reblogAccountAvatar = timeline[i]['reblog']['account']['avatar']; + accountUsername = timeline[i]['reblog']['account']['username']; + postDate = this.formatDate(timeline[i]['reblog']['created_at']); + postContent = this.formatPostText(timeline[i]['reblog']['content']); + spoilerText = timeline[i]['reblog']['spoiler_text']; + mediaContent = timeline[i]['reblog']['media_attachments']; + previewLink = timeline[i]['card']; + poll = timeline[i]['poll']; + } else { + postUrl = timeline[i]['url']; + accountUrl = timeline[i]['account']['url']; + accountAvatar = timeline[i]['account']['avatar']; + reblogAccountAvatar = null; + accountUsername = timeline[i]['account']['username']; + postDate = this.formatDate(timeline[i]['created_at']); + postContent = this.formatPostText(timeline[i]['content']); + spoilerText = timeline[i]['spoiler_text']; + mediaContent = timeline[i]['media_attachments']; + previewLink = timeline[i]['card']; + poll = timeline[i]['poll']; + } + const timelinePost: MastodonTimelineData = { + postUrl: postUrl, + accountUrl: accountUrl, + accountAvatar: accountAvatar, + reblogAccountAvatar: reblogAccountAvatar, + accountUsername: accountUsername, + postDate: postDate, + postContent: postContent, + spoilerText: spoilerText, + mediaContent: mediaContent, + previewLink: previewLink, + poll: poll, + }; + this.timelineData.push(timelinePost); + } + }); + } + + /** + * Format date + * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) + * @returns {string} Date formated (MM DD, YYYY) + */ + formatDate(d) { + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + let date = new Date(d); + + const displayDate = monthNames[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear(); + + return displayDate; + } + + formatPostText(content) { + content = content.replaceAll('rel="tag"', 'rel="tag" target="_blank"'); + content = content.replaceAll('class="u-url mention"', 'class="u-url mention" target="_blank"'); + content = content.replaceAll('class="invisible"', 'class="hide"'); + return content; + } +} diff --git a/src/app/shared/mastodon/mastodon.service.ts b/src/app/shared/mastodon/mastodon.service.ts new file mode 100644 index 0000000000..499eac3d0d --- /dev/null +++ b/src/app/shared/mastodon/mastodon.service.ts @@ -0,0 +1,182 @@ +/** + * Copyright 2023 OICR + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Mastodon embed feed timeline v3.8.2 + * More info at: + * https://gitlab.com/idotj/mastodon-embed-feed-timeline + */ + +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class MastodonService { + DEFAULT_THEME: string; + INSTANCE_URL: string; + USER_ID: string; + PROFILE_NAME: string; + TIMELINE_TYPE: string; + HASHTAG_NAME: string; + TOOTS_LIMIT: string; + HIDE_UNLISTED: boolean; + HIDE_REBLOG: boolean; + HIDE_REPLIES: boolean; + HIDE_PREVIEW_LINK: boolean; + HIDE_EMOJIS: boolean; + MARKDOWN_BLOCKQUOTE: boolean; + TEXT_MAX_LINES: string; + LINK_SEE_MORE: string; + FETCHED_DATA: any; + mtBodyContainer: HTMLElement; + public fetchedDataSubject = new Subject>(); + + constructor() { + this.fetchedDataSubject.subscribe((value) => { + this.FETCHED_DATA = value; + }); + } + + getFetchedData() { + return this.FETCHED_DATA; + } + initialize(params: any) { + this.DEFAULT_THEME = params.defaultTheme || 'auto'; + this.INSTANCE_URL = params.instanceUrl || ''; + this.USER_ID = params.userId || ''; + this.PROFILE_NAME = this.USER_ID ? params.profileName : ''; + this.TIMELINE_TYPE = params.timelineType || 'local'; + this.HASHTAG_NAME = params.hashtagName || ''; + this.TOOTS_LIMIT = params.tootsLimit || '20'; + this.HIDE_UNLISTED = typeof params.hideUnlisted !== 'undefined' ? params.hideUnlisted : false; + this.HIDE_REBLOG = typeof params.hideReblog !== 'undefined' ? params.hideReblog : false; + this.HIDE_REPLIES = typeof params.hideReplies !== 'undefined' ? params.hideReplies : false; + this.HIDE_PREVIEW_LINK = typeof params.hidePreviewLink !== 'undefined' ? params.hidePreviewLink : false; + this.HIDE_EMOJIS = typeof params.hideEmojis !== 'undefined' ? params.hideEmojis : false; + this.MARKDOWN_BLOCKQUOTE = typeof params.markdownBlockquote !== 'undefined' ? params.markdownBllockquote : false; + this.TEXT_MAX_LINES = params.textMaxLines || '0'; + this.LINK_SEE_MORE = params.linkSeeMore; + this.FETCHED_DATA = {}; + + this.mtBodyContainer = document.getElementById(params.containerBodyId); + + this.buildTimeline(); + } + + buildTimeline() { + // Apply color theme + this.setTheme(); + + // Get server data + this.getTimelineData().then(() => { + this.fetchedDataSubject.next(this.FETCHED_DATA); + }); + } + + /** + * Set the theme style chosen by the user or by the browser/OS + */ + setTheme() { + /** + * Set the theme value in the tag using the attribute "data-theme" + * @param {string} theme Type of theme to apply: dark or light + */ + const setTheme = function (theme) { + document.documentElement.setAttribute('data-theme', theme); + }; + + if (this.DEFAULT_THEME === 'auto') { + let systemTheme = window.matchMedia('(prefers-color-scheme: dark)'); + systemTheme.matches ? setTheme('dark') : setTheme('light'); + // Update the theme if user change browser/OS preference + systemTheme.addEventListener('change', (e) => { + e.matches ? setTheme('dark') : setTheme('light'); + }); + } else { + setTheme(this.DEFAULT_THEME); + } + } + + /** + * Requests to the server to get all the data + */ + getTimelineData() { + return new Promise((resolve, reject) => { + /** + * Fetch data from server + * @param {string} url address to fetch + * @returns {object} List of objects + */ + async function fetchData(url) { + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + 'Failed to fetch the following URL: ' + + url + + '
    ' + + 'Error status: ' + + response.status + + '
    ' + + 'Error message: ' + + response.statusText + ); + } + + const data = await response.json(); + return data; + } + + // URLs to fetch + let urls = { timeline: '', emojis: '' }; + if (this.TIMELINE_TYPE === 'profile') { + urls.timeline = `${this.INSTANCE_URL}/api/v1/accounts/${this.USER_ID}/statuses?limit=${this.TOOTS_LIMIT}`; + } else if (this.TIMELINE_TYPE === 'hashtag') { + urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/tag/${this.HASHTAG_NAME}?limit=${this.TOOTS_LIMIT}`; + } else if (this.TIMELINE_TYPE === 'local') { + urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/public?local=true&limit=${this.TOOTS_LIMIT}`; + } + if (!this.HIDE_EMOJIS) { + urls.emojis = this.INSTANCE_URL + '/api/v1/custom_emojis'; + } + + const urlsPromises = Object.entries(urls).map(([key, url]) => { + return fetchData(url) + .then((data) => ({ [key]: data })) + .catch((error) => { + reject(new Error('Something went wrong fetching data')); + this.mtBodyContainer.innerHTML = + '

    Sorry, request failed:
    ' + + error.message + + '
    '; + this.mtBodyContainer.setAttribute('role', 'none'); + return { [key]: [] }; + }); + }); + + // Fetch all urls simultaneously + Promise.all(urlsPromises).then((dataObjects) => { + this.FETCHED_DATA = dataObjects.reduce((result, dataItem) => { + return { ...result, ...dataItem }; + }, {}); + + resolve(); + }); + }); + } +} diff --git a/src/app/shared/twitter.service.spec.ts b/src/app/shared/twitter.service.spec.ts deleted file mode 100644 index d0d12dd233..0000000000 --- a/src/app/shared/twitter.service.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 OICR - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { inject, TestBed } from '@angular/core/testing'; - -import { TwitterService } from './twitter.service'; - -describe('TwitterService', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [TwitterService], - }); - }); - - it('should be created', inject([TwitterService], (service: TwitterService) => { - expect(service).toBeTruthy(); - })); -}); diff --git a/src/app/shared/twitter.service.ts b/src/app/shared/twitter.service.ts deleted file mode 100644 index f366ee4593..0000000000 --- a/src/app/shared/twitter.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2017 OICR - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ElementRef, Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - -/** - * Handle twitter-related actions - * See https://github.com/ABD-dev/ngx-twitter-timeline - * - * @export - * @class TwitterService - */ -@Injectable() -export class TwitterService { - private TWITTER_SCRIPT_ID = 'twitter-wjs'; - private TWITTER_WIDGET_URL = 'https://platform.twitter.com/widgets.js'; - - loadScript(): Observable { - return new Observable((observer) => { - this.startScriptLoad(); - - window['twttr'].ready((twttr) => { - observer.next(twttr); - observer.complete(); - }); - }); - } - - private startScriptLoad() { - window['twttr'] = (function (d, s, id, url) { - let script; - const firstScriptEl = d.getElementsByTagName(s)[0], - twitterScript = window['twttr'] || {}; - if (d.getElementById(id)) { - return twitterScript; - } - - script = d.createElement(s); - script.id = id; - script.src = url; - firstScriptEl.parentNode.insertBefore(script, firstScriptEl); - - twitterScript._e = []; - - twitterScript.ready = function (f) { - twitterScript._e.push(f); - }; - - return twitterScript; - })(document, 'script', this.TWITTER_SCRIPT_ID, this.TWITTER_WIDGET_URL); - } - - createTimeline(element: ElementRef, tweetLimit: number) { - const nativeElement = element.nativeElement; - nativeElement.innerHTML = ''; - window['twttr'].widgets - .createTimeline({ sourceType: 'url', url: 'https://twitter.com/dockstoreOrg' }, nativeElement, { - theme: 'light', - tweetLimit: tweetLimit, - chrome: 'nofooter', - height: 500, - }) - .catch((error) => console.error(error)); - } -} diff --git a/src/app/test/router.module.ts b/src/app/test/router.module.ts index 660630d550..aee4a0c778 100644 --- a/src/app/test/router.module.ts +++ b/src/app/test/router.module.ts @@ -8,5 +8,6 @@ import { RouterLinkStubDirective, RouterOutletStubComponent } from './router-stu @NgModule({ imports: [AppModule], declarations: [RouterLinkStubDirective, RouterOutletStubComponent], + exports: [RouterLinkStubDirective], }) export class RouterModule {} diff --git a/src/assets/images/dockstore/mastodon.svg b/src/assets/images/dockstore/mastodon.svg new file mode 100644 index 0000000000..39a116b223 --- /dev/null +++ b/src/assets/images/dockstore/mastodon.svg @@ -0,0 +1,3 @@ + + +