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 @@
-
\ 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