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