diff --git a/src/app/components/stream/hashtag/hashtag.component.html b/src/app/components/stream/hashtag/hashtag.component.html index f6df8687..9e6c2403 100644 --- a/src/app/components/stream/hashtag/hashtag.component.html +++ b/src/app/components/stream/hashtag/hashtag.component.html @@ -2,15 +2,16 @@

#{{hashtagElement.tag}}

- + +
- \ No newline at end of file diff --git a/src/app/components/stream/hashtag/hashtag.component.scss b/src/app/components/stream/hashtag/hashtag.component.scss index 15c21f51..cdf3620f 100644 --- a/src/app/components/stream/hashtag/hashtag.component.scss +++ b/src/app/components/stream/hashtag/hashtag.component.scss @@ -40,6 +40,14 @@ $inner-column-size: 320px; border: 1px solid black; color: white; } + &__follow-button { + position: absolute; + top: 7px; + right: 100px; + padding: 0 10px 0 10px; + border: 1px solid black; + color: white; + } } .hashtag-stream { diff --git a/src/app/components/stream/hashtag/hashtag.component.ts b/src/app/components/stream/hashtag/hashtag.component.ts index 98dd16b0..c1558e15 100644 --- a/src/app/components/stream/hashtag/hashtag.component.ts +++ b/src/app/components/stream/hashtag/hashtag.component.ts @@ -1,11 +1,12 @@ import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core'; -import { Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, Observable } from 'rxjs'; import { Store } from '@ngxs/store'; import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state'; import { OpenThreadEvent, ToolsService } from '../../../services/tools.service'; import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component'; import { AccountInfo } from '../../../states/accounts.state'; +import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service'; @Component({ selector: 'app-hashtag', @@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy { @Output() browseThreadEvent = new EventEmitter(); private _hashtagElement: StreamElement; - @Input() + @Input() set hashtagElement(hashtagElement: StreamElement){ this._hashtagElement = hashtagElement; this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0]; @@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy { get hashtagElement(): StreamElement{ return this._hashtagElement; } - + @ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent; @@ -38,12 +39,25 @@ export class HashtagComponent implements OnInit, OnDestroy { private lastUsedAccount: AccountInfo; private refreshSubscription: Subscription; private goToTopSubscription: Subscription; + isHashtagFollowingAvailable: boolean; + isFollowingHashtag: boolean; + + private accounts$: Observable; + + private accountSub: Subscription; + + followingLoading: boolean; + unfollowingLoading: boolean; columnAdded: boolean; constructor( private readonly store: Store, - private readonly toolsService: ToolsService) { } + private readonly toolsService: ToolsService, + private readonly mastodonService: MastodonWrapperService) { + this.accounts$ = this.store.select(state => state.registeredaccounts.accounts); + } + ngOnInit() { if(this.refreshEventEmitter) { @@ -57,11 +71,22 @@ export class HashtagComponent implements OnInit, OnDestroy { this.goToTop(); }) } + this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0]; + this.updateHashtagFollowStatus(this.lastUsedAccount); + + this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { + const selectedAccounts = accounts.filter(x => x.isSelected); + if (selectedAccounts.length > 0) { + this.lastUsedAccount = selectedAccounts[0]; + this.updateHashtagFollowStatus(this.lastUsedAccount); + } + }); } ngOnDestroy(): void { if(this.refreshSubscription) this.refreshSubscription.unsubscribe(); if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe(); + if (this.accountSub) this.accountSub.unsubscribe(); } goToTop(): boolean { @@ -83,6 +108,10 @@ export class HashtagComponent implements OnInit, OnDestroy { refresh(): any { this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0]; + this.updateHashtagFollowStatus(this.lastUsedAccount); + if (this.isHashtagFollowingAvailable) { + this.checkIfFollowingHashtag(this.lastUsedAccount); + } this.appStreamStatuses.refresh(); } @@ -99,4 +128,41 @@ export class HashtagComponent implements OnInit, OnDestroy { browseThread(openThreadEvent: OpenThreadEvent): void { this.browseThreadEvent.next(openThreadEvent); } + + private updateHashtagFollowStatus(account: AccountInfo): void { + this.toolsService.getInstanceInfo(account).then(instanceInfo => { + if (instanceInfo.major >= 4) { + this.isHashtagFollowingAvailable = true; + this.checkIfFollowingHashtag(account); + } else { + this.isHashtagFollowingAvailable = false; + } + }); + } + + private checkIfFollowingHashtag(account: AccountInfo): void { + this.mastodonService.getHashtag(account, this.hashtagElement.tag).then(tag => { + this.isFollowingHashtag = tag.following; + }); + } + + followThisHashtag(event): boolean { + this.followingLoading = true; + event.stopPropagation(); + this.mastodonService.followHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => { + this.isFollowingHashtag = tag.following; + this.followingLoading = false; + }); + return false + } + + unfollowThisHashtag(event): boolean { + this.unfollowingLoading = true; + event.stopPropagation(); + this.mastodonService.unfollowHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => { + this.isFollowingHashtag = tag.following; + this.unfollowingLoading = false; + }); + return false + } } diff --git a/src/app/services/mastodon-wrapper.service.ts b/src/app/services/mastodon-wrapper.service.ts index 63e0b689..fa7ab4f1 100644 --- a/src/app/services/mastodon-wrapper.service.ts +++ b/src/app/services/mastodon-wrapper.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngxs/store'; -import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData } from "./models/mastodon.interfaces"; +import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData, Tag } from "./models/mastodon.interfaces"; import { AccountInfo, UpdateAccount } from '../states/accounts.state'; import { StreamTypeEnum, StreamElement } from '../states/streams.state'; import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service'; @@ -12,7 +12,7 @@ import { SettingsService } from './settings.service'; @Injectable({ providedIn: 'root' }) -export class MastodonWrapperService { +export class MastodonWrapperService { private refreshingToken: { [id: string]: Promise } = {}; constructor( @@ -29,7 +29,7 @@ export class MastodonWrapperService { let isExpired = false; let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id); - if(!storedAccountInfo || !(storedAccountInfo.token)) + if(!storedAccountInfo || !(storedAccountInfo.token)) return Promise.resolve(accountInfo); try { @@ -39,7 +39,7 @@ export class MastodonWrapperService { } else { const nowEpoch = Date.now() / 1000 | 0; - //Pleroma workaround + //Pleroma workaround let expire_in = storedAccountInfo.token.expires_in; if (expire_in < 3600) { expire_in = 3600; @@ -74,7 +74,7 @@ export class MastodonWrapperService { p.then(() => { this.refreshingToken[accountInfo.id] = null; }); - + this.refreshingToken[accountInfo.id] = p; return p; } else { @@ -267,6 +267,27 @@ export class MastodonWrapperService { }); } + followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + return this.refreshAccountIfNeeded(currentlyUsedAccount) + .then((refreshedAccount: AccountInfo) => { + return this.mastodonService.followHashtag(refreshedAccount, hashtag); + }); + } + + unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + return this.refreshAccountIfNeeded(currentlyUsedAccount) + .then((refreshedAccount: AccountInfo) => { + return this.mastodonService.unfollowHashtag(refreshedAccount, hashtag); + }); + } + + getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + return this.refreshAccountIfNeeded(currentlyUsedAccount) + .then((refreshedAccount: AccountInfo) => { + return this.mastodonService.getHashtag(refreshedAccount, hashtag); + }); + } + uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise { return this.refreshAccountIfNeeded(account) .then((refreshedAccount: AccountInfo) => { diff --git a/src/app/services/mastodon.service.ts b/src/app/services/mastodon.service.ts index 4c5c7a3f..cfcd6e95 100644 --- a/src/app/services/mastodon.service.ts +++ b/src/app/services/mastodon.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http'; import { ApiRoutes } from './models/api.settings'; -import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus } from "./models/mastodon.interfaces"; +import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, Tag } from "./models/mastodon.interfaces"; import { AccountInfo } from '../states/accounts.state'; import { StreamTypeEnum, StreamElement } from '../states/streams.state'; @@ -289,6 +289,24 @@ export class MastodonService { } + followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.followHashtag}`.replace('{0}', hashtag); + const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` }); + return this.httpClient.post(route, null, { headers: headers }).toPromise(); + } + + unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.unfollowHashtag}`.replace('{0}', hashtag); + const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` }); + return this.httpClient.post(route, null, { headers: headers }).toPromise(); + } + + getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise { + const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.getHashtag}`.replace('{0}', hashtag); + const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` }); + return this.httpClient.get(route, { headers: headers }).toPromise(); + } + uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise { let input = new FormData(); input.append('file', file); @@ -382,10 +400,10 @@ export class MastodonService { addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise { let route = `https://${account.instance}${this.apiRoutes.addAccountToList}`.replace('{0}', listId); route += `?account_ids[]=${accountId}`; - + let data = new ListAccountData(); data.account_ids.push(accountId.toString()); - + const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); return this.httpClient.post(route, data, { headers: headers }).toPromise(); } diff --git a/src/app/services/models/api.settings.ts b/src/app/services/models/api.settings.ts index 08573c16..f25da639 100644 --- a/src/app/services/models/api.settings.ts +++ b/src/app/services/models/api.settings.ts @@ -75,4 +75,7 @@ export class ApiRoutes { getBookmarks = '/api/v1/bookmarks'; getFollowers = '/api/v1/accounts/{0}/followers'; getFollowing = '/api/v1/accounts/{0}/following'; + followHashtag = '/api/v1/tags/{0}/follow'; + unfollowHashtag = '/api/v1/tags/{0}/unfollow'; + getHashtag = '/api/v1/tags/{0}'; } diff --git a/src/app/services/models/mastodon.interfaces.ts b/src/app/services/models/mastodon.interfaces.ts index 8debd55a..84ec5bd3 100644 --- a/src/app/services/models/mastodon.interfaces.ts +++ b/src/app/services/models/mastodon.interfaces.ts @@ -141,7 +141,7 @@ export interface Relationship { id: number; following: boolean; followed_by: boolean; - blocked_by: boolean; + blocked_by: boolean; blocking: boolean; domain_blocking: boolean; muting: boolean; @@ -190,7 +190,7 @@ export interface Status { muted: boolean; bookmarked: boolean; card: Card; - poll: Poll; + poll: Poll; pleroma: PleromaStatusInfo; } @@ -249,4 +249,17 @@ export interface StatusParams { visibility: 'public' | 'unlisted' | 'private' | 'direct'; scheduled_at: string; application_id: string; +} + +export interface TagHistory { + day: string; + uses: number; + accounts: number; +} + +export interface Tag { + name: string; + url: string; + history: TagHistory[]; + following: boolean; } \ No newline at end of file