Merge branch 'develop' into topic_edit-status
This commit is contained in:
commit
0ce8be99bd
|
@ -3,7 +3,7 @@ cache:
|
|||
#- node_modules
|
||||
environment:
|
||||
GH_TOKEN:
|
||||
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
|
||||
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
|
||||
matrix:
|
||||
- nodejs_version: 10.9.0
|
||||
install:
|
||||
|
|
|
@ -28,6 +28,7 @@ export abstract class TimelineBase extends BrowseBase {
|
|||
statuses: StatusWrapper[] = [];
|
||||
bufferStream: Status[] = [];
|
||||
protected bufferWasCleared: boolean;
|
||||
numNewItems: number;
|
||||
streamPositionnedAtTop: boolean = true;
|
||||
protected isProcessingInfiniteScroll: boolean;
|
||||
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
<div class="hashtag-header">
|
||||
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
||||
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
|
||||
|
||||
<button *ngIf="isHashtagFollowingAvailable && !isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="followThisHashtag($event)" title="follow hashtag" [disabled]="followingLoading">follow</button>
|
||||
<button *ngIf="isHashtagFollowingAvailable && isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="unfollowThisHashtag($event)" title="unfollow hashtag" [disabled]="unfollowingLoading">unfollow</button>
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
|
||||
</a>
|
||||
</div>
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
|
||||
[streamElement]="hashtagElement"
|
||||
[goToTop]="goToTopSubject.asObservable()"
|
||||
[userLocked]="false"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
|
||||
</div>
|
|
@ -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 {
|
||||
|
|
|
@ -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<OpenThreadEvent>();
|
||||
|
||||
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<AccountInfo[]>;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__new-notification"
|
||||
<div class="stream-toots__new-notification"
|
||||
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0 && !streamPositionnedAtTop"></div>
|
||||
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ bufferStream.length }} new item<span *ngIf="bufferStream.length > 1">s</span></a>
|
||||
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 0">
|
||||
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ numNewItems }} new item<span *ngIf="numNewItems > 1">s</span></a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
|
||||
<app-status
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
|
||||
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
|
|
@ -101,6 +101,8 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.numNewItems = 0;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -133,6 +135,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
private resetStream() {
|
||||
this.statuses.length = 0;
|
||||
this.bufferStream.length = 0;
|
||||
this.numNewItems = 0;
|
||||
if (this.websocketStreaming) this.websocketStreaming.dispose();
|
||||
}
|
||||
|
||||
|
@ -154,6 +157,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
this.statuses.unshift(wrapper);
|
||||
} else {
|
||||
this.bufferStream.push(update.status);
|
||||
this.numNewItems++;
|
||||
}
|
||||
}
|
||||
} else if (update.type === EventEnum.delete) {
|
||||
|
@ -201,6 +205,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
}
|
||||
|
||||
this.bufferStream.length = 0;
|
||||
this.numNewItems = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -212,7 +217,7 @@ export class StreamStatusesComponent extends TimelineBase {
|
|||
return status.filter(x => !this.isFiltered(x));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private isFiltered(status: Status): boolean {
|
||||
if (this.streamElement.hideBoosts) {
|
||||
if (status.reblog) {
|
||||
|
|
|
@ -28,6 +28,7 @@ export class ThreadComponent extends BrowseBase {
|
|||
hasContentWarnings = false;
|
||||
private remoteStatusFetchingDisabled = false;
|
||||
|
||||
numNewItems: number; //html compatibility only
|
||||
bufferStream: Status[] = []; //html compatibility only
|
||||
streamPositionnedAtTop: boolean = true; //html compatibility only
|
||||
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only
|
||||
|
|
|
@ -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';
|
||||
|
@ -274,6 +274,27 @@ export class MastodonWrapperService {
|
|||
});
|
||||
}
|
||||
|
||||
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
return this.refreshAccountIfNeeded(currentlyUsedAccount)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
return this.mastodonService.followHashtag(refreshedAccount, hashtag);
|
||||
});
|
||||
}
|
||||
|
||||
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
return this.refreshAccountIfNeeded(currentlyUsedAccount)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
return this.mastodonService.unfollowHashtag(refreshedAccount, hashtag);
|
||||
});
|
||||
}
|
||||
|
||||
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
return this.refreshAccountIfNeeded(currentlyUsedAccount)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
return this.mastodonService.getHashtag(refreshedAccount, hashtag);
|
||||
});
|
||||
}
|
||||
|
||||
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
|
||||
return this.refreshAccountIfNeeded(account)
|
||||
.then((refreshedAccount: AccountInfo) => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -333,6 +333,24 @@ export class MastodonService {
|
|||
|
||||
}
|
||||
|
||||
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
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<Tag>(route, null, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
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<Tag>(route, null, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
|
||||
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<Tag>(route, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
|
||||
let input = new FormData();
|
||||
input.append('file', file);
|
||||
|
|
|
@ -76,4 +76,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}';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -207,11 +207,6 @@ export interface PleromaStatusInfo {
|
|||
local: boolean;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface List {
|
||||
id: string;
|
||||
title: string;
|
||||
|
@ -249,4 +244,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;
|
||||
}
|
Loading…
Reference in New Issue