Merge pull request #4 from NicolasConstant/topic-start-column-handling

Topic start column handling
This commit is contained in:
Nicolas Constant 2018-09-16 14:05:50 -04:00 committed by GitHub
commit 6280409a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 438 additions and 346 deletions

View File

@ -14,7 +14,7 @@ export class AppComponent implements OnInit, OnDestroy{
title = 'app';
private floatingColumnActive: boolean;
floatingColumnActive: boolean;
private columnEditorSub: Subscription;
constructor(private readonly navigationService: NavigationService) {

View File

@ -15,10 +15,8 @@ import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.c
import { StreamsMainDisplayComponent } from "./pages/streams-main-display/streams-main-display.component";
import { StreamComponent } from "./components/stream/stream.component";
import { StreamsSelectionFooterComponent } from "./components/streams-selection-footer/streams-selection-footer.component";
import { TootComponent } from "./components/toot/toot.component";
import { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
import { AuthService } from "./services/auth.service";
import { AccountsService } from "./services/accounts.service";
import { StreamingService } from "./services/streaming.service";
import { RegisteredAppsState } from "./states/registered-apps.state";
import { AccountsState } from "./states/accounts.state";
@ -28,6 +26,8 @@ import { FloatingColumnComponent } from './components/floating-column/floating-c
import { ColumnsEditorComponent } from './components/floating-column/columns-editor/columns-editor.component';
import { MessageEditorComponent } from './components/floating-column/message-editor/message-editor.component';
import { StreamsState } from "./states/streams.state";
import { StatusComponent } from "./components/stream/status/status.component";
import { MastodonService } from "./services/mastodon.service";
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
@ -43,7 +43,7 @@ const routes: Routes = [
StreamsMainDisplayComponent,
StreamComponent,
StreamsSelectionFooterComponent,
TootComponent,
StatusComponent,
RegisterNewAccountComponent,
AccountIconComponent,
FloatingColumnComponent,
@ -65,11 +65,7 @@ const routes: Routes = [
]),
NgxsStoragePluginModule.forRoot()
],
providers: [AuthService, NavigationService, AccountsService, StreamingService, { provide: APP_INITIALIZER, useFactory: settingsServiceFactory, deps: [AccountsService], multi: true }],
providers: [AuthService, NavigationService, MastodonService, StreamingService],
bootstrap: [AppComponent]
})
export class AppModule { }
function settingsServiceFactory(service: AccountsService) {
return () => service.load();
}

View File

@ -4,9 +4,9 @@ import { Store } from "@ngxs/store";
import { Account } from "../../services/models/mastodon.interfaces";
import { AccountWrapper } from "../../models/account.models";
import { AccountsService } from "../../services/accounts.service";
import { AccountsStateModel, AccountInfo } from "../../states/accounts.state";
import { NavigationService } from "../../services/navigation.service";
import { MastodonService } from "../../services/mastodon.service";
@Component({
@ -23,7 +23,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
constructor(
private readonly navigationService: NavigationService,
private readonly accountsService: AccountsService,
private readonly mastodonService: MastodonService,
private readonly store: Store) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
@ -42,7 +42,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.accounts.push(accWrapper);
this.loadedAccounts[accWrapper.username] = acc;
this.accountsService.retrieveAccountDetails(acc)
this.mastodonService.retrieveAccountDetails(acc)
.then((result: Account) => {
accWrapper.avatar = result.avatar;
});

View File

@ -24,7 +24,7 @@ export class AccountIconComponent implements OnInit {
return false;
}
openMenu(event): boolean {
openMenu(): boolean {
this.openMenuNotify.emit(this.account);
return false;
}

View File

@ -0,0 +1,5 @@
<div class="toot">
<img class="toot__avatar" src="{{ status.account.avatar }}" />
<a href class="toot__profile-link"><span class="toot__fullname" innerHTML="{{status.account.display_name}}"></span> @<span id="toot-username" innerHTML="{{status.account.username}}"></span></a>
<div class="toot__content" innerHTML="{{status.content}}"></div>
</div>

View File

@ -0,0 +1,47 @@
.toot {
border: solid #06070b;
border-width: 0 0 1px 0;
margin: 0;
padding: 0;
width: calc(100%);
min-height: 70px;
overflow: hidden;
&__avatar {
margin: 10px 0 0 10px;
/* margin: 0; */
width: 50px;
height: 50px;
float: left;
}
&__fullname {
color: white;
}
&__profile-link {
color: #353e64;
margin: 7px 0 0 70px;
display: block;
}
&__content {
/*width: calc(100% - 50px);*/
margin: 10px 10px 10px 70px;
}
&__content p {
margin: 0;
font-size: 0.85em;
}
}
// #toot-avatar img {
// width: 50px;
// height: 50px;
// border-radius: 2px;
// margin: 0;
// }
/* #toot-username {
color: grey;
} */
/* ::ng-deep .invisible {
display: inline;
color: red;
} */

View File

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TootComponent } from './toot.component';
import { StatusComponent } from './status.component';
describe('TootComponent', () => {
let component: TootComponent;
let fixture: ComponentFixture<TootComponent>;
describe('StatusComponent', () => {
let component: StatusComponent;
let fixture: ComponentFixture<StatusComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TootComponent ]
declarations: [ StatusComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TootComponent);
fixture = TestBed.createComponent(StatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,18 @@
import { Component, OnInit, Input } from "@angular/core";
import { Status } from "../../../services/models/mastodon.interfaces";
@Component({
selector: "app-status",
templateUrl: "./status.component.html",
styleUrls: ["./status.component.scss"]
})
export class StatusComponent implements OnInit {
@Input() status: Status;
constructor() { }
ngOnInit() {
}
}

View File

@ -1,10 +1,10 @@
<div class="stream-column">
<div class="stream-column__stream-header">
<a href title="return to top" (click)="goToTop()"><h1>{{ stream.streamName.toUpperCase() }}</h1></a>
<a href title="return to top" (click)="goToTop()"><h1>{{ streamElement.name.toUpperCase() }}</h1></a>
</div>
<div class="stream-toots" data-simplebar>
<div *ngFor="let toot of toots">
<app-toot [toot]="toot"></app-toot>
<div class="stream-toots flexcroll" #statusstream> <!-- data-simplebar -->
<div *ngFor="let status of statuses">
<app-status [status]="status" ></app-status>
</div>
</div>
</div>

View File

@ -29,3 +29,22 @@
width: 320px;
overflow: auto;
}
.flexcroll {
scrollbar-face-color: #08090d;
scrollbar-shadow-color: #08090d;
scrollbar-highlight-color: #08090d;
scrollbar-3dlight-color: #08090d;
scrollbar-darkshadow-color: #08090d;
scrollbar-track-color: #08090d;
scrollbar-arrow-color: #08090d;
&::-webkit-scrollbar {
width: 7px;
}
&::-webkit-scrollbar-thumb {
-webkit-border-radius: 0px;
border-radius: 0px;
background: #08090d;
-webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
}
}

View File

@ -1,56 +1,84 @@
import { Component, OnInit, Input } from "@angular/core";
import { Stream, TootWrapper } from "../../models/stream.models";
import { Component, OnInit, Input, ElementRef, ViewChild } from "@angular/core";
import { AccountWrapper } from "../../models/account.models";
import { StreamElement, StreamTypeEnum } from "../../states/streams.state";
import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service";
import { Store } from "@ngxs/store";
import { AccountInfo } from "../../states/accounts.state";
import { Status } from "../../services/models/mastodon.interfaces";
import { MastodonService } from "../../services/mastodon.service";
@Component({
selector: "app-stream",
templateUrl: "./stream.component.html",
styleUrls: ["./stream.component.scss"]
selector: "app-stream",
templateUrl: "./stream.component.html",
styleUrls: ["./stream.component.scss"]
})
export class StreamComponent implements OnInit {
private _stream: Stream;
private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
@Input()
set stream(stream: Stream) {
this._stream = stream;
this._stream.statuses.subscribe((toots: TootWrapper[]) => {
for (let t of toots) {
this.toots.push(t);
}
});
}
statuses: Status[] = [];
get stream(): Stream {
return this._stream;
}
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
toots: TootWrapper[] = [];
const splitedUserName = streamElement.username.split('@');
const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
constructor(){
// var simplebar = new SimpleBar(document.querySelector('#mam-stream-toots'), { autoHide: true });
}
this.retrieveToots(); //TODO change this for WebSockets
this.launchWebsocket();
}
get streamElement(): StreamElement {
return this._streamElement;
}
ngOnInit() {
//Stubs
//const newStream = new Stream();
//newStream.streamName = "Stream Name";
//this.stream = newStream;
constructor(
private readonly store: Store,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
}
//const acc1 = new AccountWrapper();
//acc1.username = "@mastodon.social@Gargron";
//acc1.avatar = "https://files.mastodon.social/accounts/avatars/000/000/001/original/4df197532c6b768c.png";
ngOnInit() {
}
//for (let i = 0; i < 20; i++) {
// const newToot = new TootWrapper();
// newToot.account = acc1;
// newToot.content = "Lorem Elsass ipsum tristique semper elit jetz gehts los lacus habitant Hans sagittis baeckeoffe condimentum id, salu bredele ch'ai libero, ftomi! hop Pfourtz ! id munster auctor, Miss Dahlias rhoncus Yo dû. Salu bissame turpis ante amet non sed gal Spätzle Gal !";
// this.toots.push(newToot);
//}
}
@ViewChild('statusstream') public statustream: ElementRef;
goToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
return false;
}
goToTop(): boolean {
return false;
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
}
private retrieveToots(): void {
this.mastodonService.getTimeline(this.account, this._streamElement.type)
.then((results: Status[]) => {
for (const s of results) {
this.statuses.push(s);
}
});
}
private launchWebsocket(): void {
this.websocketStreaming = this.streamingService.getStreaming(this.account, this._streamElement.type);
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (update) {
if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.id == update.status.id)) {
this.statuses.unshift(update.status);
}
}
}
});
}
}

View File

@ -26,9 +26,7 @@ export class StreamsSelectionFooterComponent implements OnInit {
}
onColumnSelection(index: number): boolean {
console.warn(`column selected: ${index}`);
this.navigationService.columnSelected(index);
return false;
}
}

View File

@ -1,8 +0,0 @@
<div id="toot">
<div id="toot-avatar">
<img src="{{ toot.account.avatar }}" />
</div>
<a href id="toot-profile-link"><span id="toot-fullname" innerHTML="{{toot.account.display_name}}"></span> @<span id="toot-username" innerHTML="{{toot.account.username}}"></span></a>
<div id="toot-content" innerHTML="{{toot.content}}">
</div>
</div>

View File

@ -1,54 +0,0 @@
#toot {
border: solid #06070b;
border-width: 0 0 1px 0;
margin: 0;
padding: 0;
width: calc(100%);
min-height: 70px;
overflow: hidden;
}
#toot-avatar {
margin: 10px 0 0 10px;
/* margin: 0; */
width: 50px;
height: 50px;
float: left;
}
#toot-avatar img {
width: 50px;
height: 50px;
border-radius: 4px;
margin: 0;
}
#toot-fullname {
color: white;
}
/* #toot-username {
color: grey;
} */
#toot-profile-link {
color: #353e64;
margin: 7px 0 0 70px;
display: block;
}
#toot-content {
/*width: calc(100% - 50px);*/
margin: 10px 10px 10px 70px ;
}
#toot-content p {
margin: 0;
font-size: 0.85em;
}
/* ::ng-deep .invisible {
display: inline;
color: red;
} */

View File

@ -1,17 +0,0 @@
import { Component, OnInit, Input } from "@angular/core";
import { TootWrapper } from "../../models/stream.models";
@Component({
selector: "app-toot",
templateUrl: "./toot.component.html",
styleUrls: ["./toot.component.scss"]
})
export class TootComponent implements OnInit {
@Input() toot: TootWrapper;
constructor() { }
ngOnInit() {
}
}

View File

@ -1,80 +0,0 @@
import { Http, Headers, Response } from "@angular/http";
import { BehaviorSubject } from "rxjs";
import { AccountWrapper } from "./account.models";
// import { LocalAccount } from "../services/accounts.service";
import { ApiRoutes } from "../services/models/api.settings";
import { Account, Status } from "../services/models/mastodon.interfaces";
import { StreamingService, StreamingWrapper } from "../services/streaming.service";
import { StreamTypeEnum } from "../states/streams.state";
export class Stream {
private apiRoutes = new ApiRoutes();
statuses = new BehaviorSubject<TootWrapper[]>([]);
constructor(
private readonly httpService: Http,
public streamName: string,
private readonly type: StreamTypeEnum) {
this.retrieveToots(); //TODO change this for WebSockets
}
private test: StreamingWrapper;
private retrieveToots(): void {
// //TEST
// const service = new StreamingService();
// this.test = service.getStreaming(this.account.mastodonInstance, this.account.tokenData.access_token);
// //END TEST
const route = this.getTimelineRoute();
const header = new Headers();
// header.append("Authorization", `Bearer ${this.account.tokenData.access_token}`);
// this.httpService.get(this.account.mastodonInstance + route, { headers: header }).toPromise()
// .then((res: Response) => {
// const statuses = (res.json() as Status[])
// .map((status: Status) => {
// return new TootWrapper(status);
// });
// this.statuses.next(statuses);
// });
}
private getTimelineRoute(): string {
switch (this.type) {
case StreamTypeEnum.personnal:
return this.apiRoutes.getHomeTimeline;
case StreamTypeEnum.local:
return this.apiRoutes.getPublicTimeline + `?Local=true`;
case StreamTypeEnum.global:
return this.apiRoutes.getPublicTimeline + `?Local=false`;
}
}
}
// export enum StreamTypeEnum {
// Home,
// Public,
// Local
// }
export class TootWrapper {
constructor(status: Status) {
this.account = new AccountWrapper();
this.account.username = status.account.username;
this.account.display_name = status.account.display_name;
this.account.avatar = status.account.avatar;
this.content = status.content;
}
account: AccountWrapper; //TODO change to Account
content: string;
}

View File

@ -5,9 +5,9 @@ import { Observable } from "rxjs";
import { AuthService } from "../../services/auth.service";
import { TokenData, AppData } from "../../services/models/mastodon.interfaces";
import { AccountsService } from "../../services/accounts.service";
import { AddRegisteredApp, RegisteredAppsState, RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { AccountInfo, AddAccount } from "../../states/accounts.state";
import { MastodonService } from "../../services/mastodon.service";
@Component({
selector: "app-register-new-account",
@ -23,7 +23,6 @@ export class RegisterNewAccountComponent implements OnInit {
constructor(
private readonly authService: AuthService,
private readonly accountsService: AccountsService,
private readonly store: Store,
private readonly activatedRoute: ActivatedRoute) {

View File

@ -1,5 +1,5 @@
<div class="main-display flexcroll">
<div class="main-display__stream-column" *ngFor="let s of streams">
<app-stream [stream]="s" #stream></app-stream>
<div class="main-display__stream-column" *ngFor="let s of streamElements$ | async">
<app-stream [streamElement]="s" #stream></app-stream>
</div>
</div>

View File

@ -1,12 +1,9 @@
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ElementRef } from "@angular/core";
import { Stream } from "../../models/stream.models";
import { Component, OnInit, OnDestroy, QueryList, ViewChildren, ElementRef } from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { StreamElement } from "../../states/streams.state";
import { Store } from "@ngxs/store";
import { Http } from "@angular/http";
import { NavigationService } from "../../services/navigation.service";
import { Select } from "@ngxs/store";
import { StreamElement } from "../../states/streams.state";
import { NavigationService } from "../../services/navigation.service";
@Component({
selector: "app-streams-main-display",
@ -15,51 +12,29 @@ import { NavigationService } from "../../services/navigation.service";
})
export class StreamsMainDisplayComponent implements OnInit, OnDestroy {
streams: Stream[] = [];
private streams$: Observable<StreamElement[]>;
private streamsStateSub: Subscription;
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
private columnSelectedSub: Subscription;
constructor(
private readonly navigationService: NavigationService,
private readonly http: Http,
private readonly store: Store) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
private readonly navigationService: NavigationService) {
}
ngOnInit() {
this.streamsStateSub = this.streams$.subscribe((streams: StreamElement[]) => {
this.streams.length = 0;
for (const stream of streams) {
const newStream = new Stream(this.http, stream.name, stream.type);
this.streams.push(newStream);
}
this.columnSelectedSub = this.navigationService.columnSelectedSubject.subscribe((columnIndex: number) => {
this.focusOnColumn(columnIndex);
});
this.columnSelectedSub = this.navigationService.columnSelectedSubject.subscribe((columnIndex: number) => {
this.focusOnColumn(columnIndex);
});
}
ngOnDestroy(): void {
this.streamsStateSub.unsubscribe();
this.columnSelectedSub.unsubscribe();
}
@ViewChildren('stream', { read: ElementRef }) public streamsElementRef: QueryList<ElementRef>;;
private focusOnColumn(columnIndex: number): void {
console.warn(`col selected: ${columnIndex}`);
if (columnIndex > -1) {
console.warn(this.streamsElementRef);
setTimeout(() => {
this.streamsElementRef.toArray()[columnIndex].nativeElement.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
});
}
}
}

View File

@ -1,26 +0,0 @@
import { Injectable } from "@angular/core";
import { Http, Headers, Response } from "@angular/http";
import { Subject, BehaviorSubject } from "rxjs";
import { TokenData, Account } from "./models/mastodon.interfaces";
import { ApiRoutes } from "./models/api.settings";
import { AccountInfo } from "../states/accounts.state";
import { HttpClient, HttpHeaders } from "@angular/common/http";
@Injectable()
export class AccountsService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) {}
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
const headers = new HttpHeaders({'Authorization':`Bearer ${account.token.access_token}`});
// const headers = new HttpHeaders({'Bearer':`${account.token}`});
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, {headers: headers}).toPromise();
}
load(): any {
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { MastodonService } from './mastodon.service';
describe('MastodonService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MastodonService]
});
});
it('should be created', inject([MastodonService], (service: MastodonService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,70 @@
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings';
import { Account, Status } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum } from '../states/streams.state';
@Injectable()
export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { }
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise();
}
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20): Promise<Status[]> {
const route = `https://${account.instance}${this.getTimelineRoute(type, max_id, since_id, limit)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
}
private getTimelineRoute(type: StreamTypeEnum, max_id: string, since_id: string, limit: number): string {
let route: string;
switch (type) {
case StreamTypeEnum.personnal:
route = this.apiRoutes.getHomeTimeline;
break;
case StreamTypeEnum.local:
route = this.apiRoutes.getPublicTimeline + `?local=true&`;
break;
case StreamTypeEnum.global:
route = this.apiRoutes.getPublicTimeline + `?local=false&`;
break;
case StreamTypeEnum.directmessages:
route = this.apiRoutes.getDirectTimeline;
break;
case StreamTypeEnum.tag:
route = this.apiRoutes.getTagTimeline.replace('{0}', 'TODO');
break;
case StreamTypeEnum.list:
route = this.apiRoutes.getListTimeline.replace('{0}', 'TODO');
break;
default:
throw new Error('StreamTypeEnum not supported');
}
if (!route.includes('?')) route = route + '?';
if (max_id) route = route + `max_id=${max_id}&`;
if (since_id) route = route + `since_id=${since_id}&`;
if (limit) route = route + `limit=${limit}&`;
return this.trimChar(this.trimChar(route, '?'), '&');
}
private escapeRegExp(strToEscape) {
// Escape special characters for use in a regular expression
return strToEscape.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
private trimChar(origString, charToTrim) {
charToTrim = this.escapeRegExp(charToTrim);
var regEx = new RegExp("^[" + charToTrim + "]+|[" + charToTrim + "]+$", "g");
return origString.replace(regEx, "");
};
}

View File

@ -1,47 +1,51 @@
export class ApiRoutes {
createApp = "/api/v1/apps";
getToken = "/oauth/token";
getAccount = "/api/v1/accounts/{0}";
getCurrentAccount = "/api/v1/accounts/verify_credentials";
getAccountFollowers = "/api/v1/accounts/{0}/followers";
getAccountFollowing = "/api/v1/accounts/{0}/following";
getAccountStatuses = "/api/v1/accounts/{0}/statuses";
follow = "/api/v1/accounts/{0}/follow";
unfollow = "/api/v1/accounts/{0}/unfollow";
block = "/api/v1/accounts/{0}/block";
unblock = "/api/v1/accounts/{0}/unblock";
mute = "/api/v1/accounts/{0}/mute";
unmute = "/api/v1/accounts/{0}/unmute";
getAccountRelationships = "/api/v1/accounts/relationships";
searchForAccounts = "/api/v1/accounts/search";
getBlocks = "/api/v1/blocks";
getFavourites = "/api/v1/favourites";
getFollowRequests = "/api/v1/follow_requests";
authorizeFollowRequest = "/api/v1/follow_requests/authorize";
rejectFollowRequest = "/api/v1/follow_requests/reject";
followRemote = "/api/v1/follows";
getInstance = "/api/v1/instance";
uploadMediaAttachment = "/api/v1/media";
getMutes = "/api/v1/mutes";
getNotifications = "/api/v1/notifications";
getSingleNotifications = "/api/v1/notifications/{0}";
clearNotifications = "/api/v1/notifications/clear";
getReports = "/api/v1/reports";
reportUser = "/api/v1/reports";
search = "/api/v1/search";
getStatus = "/api/v1/statuses/{0}";
getStatusContext = "/api/v1/statuses/{0}/context";
getStatusCard = "/api/v1/statuses/{0}/card";
getStatusRebloggedBy = "/api/v1/statuses/{0}/reblogged_by";
getStatusFavouritedBy = "/api/v1/statuses/{0}/favourited_by";
postNewStatus = "/api/v1/statuses";
deleteStatus = "/api/v1/statuses/{0}";
reblogStatus = "/api/v1/statuses/{0}/reblog";
unreblogStatus = "/api/v1/statuses/{0}/unreblog";
favouritingStatus = "/api/v1/statuses/{0}/favourite";
unfavouritingStatus = "/api/v1/statuses/{0}/unfavourite";
getHomeTimeline = "/api/v1/timelines/home";
getPublicTimeline = "/api/v1/timelines/public";
getHastagTimeline = "/api/v1/timelines/tag/{0}";
createApp = '/api/v1/apps';
getToken = '/oauth/token';
getAccount = '/api/v1/accounts/{0}';
getCurrentAccount = '/api/v1/accounts/verify_credentials';
getAccountFollowers = '/api/v1/accounts/{0}/followers';
getAccountFollowing = '/api/v1/accounts/{0}/following';
getAccountStatuses = '/api/v1/accounts/{0}/statuses';
follow = '/api/v1/accounts/{0}/follow';
unfollow = '/api/v1/accounts/{0}/unfollow';
block = '/api/v1/accounts/{0}/block';
unblock = '/api/v1/accounts/{0}/unblock';
mute = '/api/v1/accounts/{0}/mute';
unmute = '/api/v1/accounts/{0}/unmute';
getAccountRelationships = '/api/v1/accounts/relationships';
searchForAccounts = '/api/v1/accounts/search';
getBlocks = '/api/v1/blocks';
getFavourites = '/api/v1/favourites';
getFollowRequests = '/api/v1/follow_requests';
authorizeFollowRequest = '/api/v1/follow_requests/authorize';
rejectFollowRequest = '/api/v1/follow_requests/reject';
followRemote = '/api/v1/follows';
getInstance = '/api/v1/instance';
uploadMediaAttachment = '/api/v1/media';
getMutes = '/api/v1/mutes';
getNotifications = '/api/v1/notifications';
getSingleNotifications = '/api/v1/notifications/{0}';
clearNotifications = '/api/v1/notifications/clear';
getReports = '/api/v1/reports';
reportUser = '/api/v1/reports';
search = '/api/v1/search';
getStatus = '/api/v1/statuses/{0}';
getStatusContext = '/api/v1/statuses/{0}/context';
getStatusCard = '/api/v1/statuses/{0}/card';
getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
postNewStatus = '/api/v1/statuses';
deleteStatus = '/api/v1/statuses/{0}';
reblogStatus = '/api/v1/statuses/{0}/reblog';
unreblogStatus = '/api/v1/statuses/{0}/unreblog';
favouritingStatus = '/api/v1/statuses/{0}/favourite';
unfavouritingStatus = '/api/v1/statuses/{0}/unfavourite';
getHomeTimeline = '/api/v1/timelines/home';
getPublicTimeline = '/api/v1/timelines/public';
getHastagTimeline = '/api/v1/timelines/tag/{0}';
getDirectTimeline = '/api/v1/timelines/direct';
getTagTimeline = '/api/v1/timelines/tag/{0}';
getListTimeline = '/api/v1/timelines/list/{0}';
getStreaming = '/api/v1/streaming?access_token={0}&stream={1}';
}

View File

@ -1,24 +1,126 @@
import { Injectable } from "@angular/core";
import { Status } from "./models/mastodon.interfaces";
import { BehaviorSubject } from "rxjs";
import { ApiRoutes } from "./models/api.settings";
import { StreamTypeEnum } from "../states/streams.state";
import { MastodonService } from "./mastodon.service";
import { AccountInfo } from "../states/accounts.state";
import { stat } from "fs";
@Injectable()
export class StreamingService {
constructor(
private readonly mastodonService: MastodonService) { }
constructor() { }
getStreaming(accountInfo: AccountInfo, streamType: StreamTypeEnum): StreamingWrapper {
return new StreamingWrapper(this.mastodonService, accountInfo, streamType);
}
//TODO restructure this to handle real domain objects
getStreaming(mastodonInstance: string, accessToken: string): StreamingWrapper {
return new StreamingWrapper(mastodonInstance.replace("https://", "wss://") + `/api/v1/streaming//?access_token=${accessToken}&stream=public`)
}
}
export class StreamingWrapper {
statusUpdateSubjet = new BehaviorSubject<StatusUpdate>(null);
eventSource: WebSocket;
private apiRoutes = new ApiRoutes();
constructor(private readonly domain: string) {
const eventSource = new WebSocket(domain);
eventSource.onmessage = x => console.warn(JSON.parse(x.data));
eventSource.onerror = x => console.error(x);
eventSource.onopen = x => console.log(x);
eventSource.onclose = x => console.log(x);
}
constructor(
private readonly mastodonService: MastodonService,
private readonly accountInfo: AccountInfo,
private readonly streamType: StreamTypeEnum) {
const request = this.getRequest(streamType);
const route = `wss://${accountInfo.instance}${this.apiRoutes.getStreaming}`.replace('{0}', accountInfo.token.access_token).replace('{1}', request);
this.start(route);
}
private start(route: string) {
this.eventSource = new WebSocket(route);
this.eventSource.onmessage = x => this.statusParsing(<WebSocketEvent>JSON.parse(x.data));
this.eventSource.onerror = x => this.webSocketGotError(x);
this.eventSource.onopen = x => console.log(x);
this.eventSource.onclose = x => this.webSocketClosed(route, x);
}
private errorClosing: boolean;
private webSocketGotError(x: Event) {
this.errorClosing = true;
}
private since_id: string;
private webSocketClosed(domain, x: Event) {
console.log(x);
if (this.errorClosing) {
this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
.then((status: Status[]) => {
// status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
status = status.sort((a, b) => a.id.localeCompare(b.id));
for (const s of status) {
const update = new StatusUpdate();
update.status = s;
update.type = EventEnum.update;
this.since_id = update.status.id;
this.statusUpdateSubjet.next(update);
}
})
.catch(err => {
console.error(err);
})
.then(() => {
setTimeout(() => { this.start(domain) }, 20 * 1000);
});
this.errorClosing = false;
} else {
setTimeout(() => { this.start(domain) }, 5000);
}
}
private statusParsing(event: WebSocketEvent) {
const newUpdate = new StatusUpdate();
switch (event.event) {
case 'update':
newUpdate.type = EventEnum.update;
newUpdate.status = <Status>JSON.parse(event.payload);
break;
case 'delete':
newUpdate.type = EventEnum.delete;
newUpdate.messageId = event.payload;
break;
default:
newUpdate.type = EventEnum.unknow;
}
this.statusUpdateSubjet.next(newUpdate);
}
private getRequest(type: StreamTypeEnum): string {
switch (type) {
case StreamTypeEnum.global:
return 'public';
case StreamTypeEnum.local:
return 'public:local';
case StreamTypeEnum.personnal:
return 'user';
}
}
}
class WebSocketEvent {
event: string;
payload: any;
}
export class StatusUpdate {
type: EventEnum;
status: Status;
messageId: number;
}
export enum EventEnum {
unknow = 0,
update = 1,
delete = 2
}

View File

@ -39,6 +39,7 @@ export class StreamElement {
favorites = 4,
activity = 5,
list = 6,
directmessages = 7,
}
directmessages = 7,
tag = 8,
}