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'; title = 'app';
private floatingColumnActive: boolean; floatingColumnActive: boolean;
private columnEditorSub: Subscription; private columnEditorSub: Subscription;
constructor(private readonly navigationService: NavigationService) { 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 { StreamsMainDisplayComponent } from "./pages/streams-main-display/streams-main-display.component";
import { StreamComponent } from "./components/stream/stream.component"; import { StreamComponent } from "./components/stream/stream.component";
import { StreamsSelectionFooterComponent } from "./components/streams-selection-footer/streams-selection-footer.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 { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
import { AuthService } from "./services/auth.service"; import { AuthService } from "./services/auth.service";
import { AccountsService } from "./services/accounts.service";
import { StreamingService } from "./services/streaming.service"; import { StreamingService } from "./services/streaming.service";
import { RegisteredAppsState } from "./states/registered-apps.state"; import { RegisteredAppsState } from "./states/registered-apps.state";
import { AccountsState } from "./states/accounts.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 { ColumnsEditorComponent } from './components/floating-column/columns-editor/columns-editor.component';
import { MessageEditorComponent } from './components/floating-column/message-editor/message-editor.component'; import { MessageEditorComponent } from './components/floating-column/message-editor/message-editor.component';
import { StreamsState } from "./states/streams.state"; import { StreamsState } from "./states/streams.state";
import { StatusComponent } from "./components/stream/status/status.component";
import { MastodonService } from "./services/mastodon.service";
const routes: Routes = [ const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" }, { path: "", redirectTo: "home", pathMatch: "full" },
@ -43,7 +43,7 @@ const routes: Routes = [
StreamsMainDisplayComponent, StreamsMainDisplayComponent,
StreamComponent, StreamComponent,
StreamsSelectionFooterComponent, StreamsSelectionFooterComponent,
TootComponent, StatusComponent,
RegisterNewAccountComponent, RegisterNewAccountComponent,
AccountIconComponent, AccountIconComponent,
FloatingColumnComponent, FloatingColumnComponent,
@ -65,11 +65,7 @@ const routes: Routes = [
]), ]),
NgxsStoragePluginModule.forRoot() NgxsStoragePluginModule.forRoot()
], ],
providers: [AuthService, NavigationService, AccountsService, StreamingService, { provide: APP_INITIALIZER, useFactory: settingsServiceFactory, deps: [AccountsService], multi: true }], providers: [AuthService, NavigationService, MastodonService, StreamingService],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } 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 { Account } from "../../services/models/mastodon.interfaces";
import { AccountWrapper } from "../../models/account.models"; import { AccountWrapper } from "../../models/account.models";
import { AccountsService } from "../../services/accounts.service";
import { AccountsStateModel, AccountInfo } from "../../states/accounts.state"; import { AccountsStateModel, AccountInfo } from "../../states/accounts.state";
import { NavigationService } from "../../services/navigation.service"; import { NavigationService } from "../../services/navigation.service";
import { MastodonService } from "../../services/mastodon.service";
@Component({ @Component({
@ -23,7 +23,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
constructor( constructor(
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService,
private readonly accountsService: AccountsService, private readonly mastodonService: MastodonService,
private readonly store: Store) { private readonly store: Store) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts); this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
@ -42,7 +42,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.accounts.push(accWrapper); this.accounts.push(accWrapper);
this.loadedAccounts[accWrapper.username] = acc; this.loadedAccounts[accWrapper.username] = acc;
this.accountsService.retrieveAccountDetails(acc) this.mastodonService.retrieveAccountDetails(acc)
.then((result: Account) => { .then((result: Account) => {
accWrapper.avatar = result.avatar; accWrapper.avatar = result.avatar;
}); });

View File

@ -24,7 +24,7 @@ export class AccountIconComponent implements OnInit {
return false; return false;
} }
openMenu(event): boolean { openMenu(): boolean {
this.openMenuNotify.emit(this.account); this.openMenuNotify.emit(this.account);
return false; 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TootComponent } from './toot.component'; import { StatusComponent } from './status.component';
describe('TootComponent', () => { describe('StatusComponent', () => {
let component: TootComponent; let component: StatusComponent;
let fixture: ComponentFixture<TootComponent>; let fixture: ComponentFixture<StatusComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ TootComponent ] declarations: [ StatusComponent ]
}) })
.compileComponents(); .compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TootComponent); fixture = TestBed.createComponent(StatusComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); 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">
<div class="stream-column__stream-header"> <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>
<div class="stream-toots" data-simplebar> <div class="stream-toots flexcroll" #statusstream> <!-- data-simplebar -->
<div *ngFor="let toot of toots"> <div *ngFor="let status of statuses">
<app-toot [toot]="toot"></app-toot> <app-status [status]="status" ></app-status>
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,3 +29,22 @@
width: 320px; width: 320px;
overflow: auto; 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,6 +1,11 @@
import { Component, OnInit, Input } from "@angular/core"; import { Component, OnInit, Input, ElementRef, ViewChild } from "@angular/core";
import { Stream, TootWrapper } from "../../models/stream.models";
import { AccountWrapper } from "../../models/account.models"; 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({ @Component({
selector: "app-stream", selector: "app-stream",
@ -8,49 +13,72 @@ import { AccountWrapper } from "../../models/account.models";
styleUrls: ["./stream.component.scss"] styleUrls: ["./stream.component.scss"]
}) })
export class StreamComponent implements OnInit { export class StreamComponent implements OnInit {
private _stream: Stream; private _streamElement: StreamElement;
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: Status[] = [];
@Input() @Input()
set stream(stream: Stream) { set streamElement(streamElement: StreamElement) {
this._stream = stream; this._streamElement = streamElement;
this._stream.statuses.subscribe((toots: TootWrapper[]) => {
for (let t of toots) { const splitedUserName = streamElement.username.split('@');
this.toots.push(t); const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
this.retrieveToots(); //TODO change this for WebSockets
this.launchWebsocket();
}
get streamElement(): StreamElement {
return this._streamElement;
}
constructor(
private readonly store: Store,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
}
ngOnInit() {
}
@ViewChild('statusstream') public statustream: ElementRef;
goToTop(): boolean {
const stream = this.statustream.nativeElement as HTMLElement;
stream.scrollTo({
top: 0,
behavior: 'smooth'
});
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);
} }
}); });
} }
get stream(): Stream { private launchWebsocket(): void {
return this._stream; 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);
} }
toots: TootWrapper[] = [];
constructor(){
// var simplebar = new SimpleBar(document.querySelector('#mam-stream-toots'), { autoHide: true });
} }
ngOnInit() {
//Stubs
//const newStream = new Stream();
//newStream.streamName = "Stream Name";
//this.stream = newStream;
//const acc1 = new AccountWrapper();
//acc1.username = "@mastodon.social@Gargron";
//acc1.avatar = "https://files.mastodon.social/accounts/avatars/000/000/001/original/4df197532c6b768c.png";
//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);
//}
} }
});
goToTop(): boolean {
return false;
} }
} }

View File

@ -26,9 +26,7 @@ export class StreamsSelectionFooterComponent implements OnInit {
} }
onColumnSelection(index: number): boolean { onColumnSelection(index: number): boolean {
console.warn(`column selected: ${index}`);
this.navigationService.columnSelected(index); this.navigationService.columnSelected(index);
return false; 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 { AuthService } from "../../services/auth.service";
import { TokenData, AppData } from "../../services/models/mastodon.interfaces"; 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 { AddRegisteredApp, RegisteredAppsState, RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { AccountInfo, AddAccount } from "../../states/accounts.state"; import { AccountInfo, AddAccount } from "../../states/accounts.state";
import { MastodonService } from "../../services/mastodon.service";
@Component({ @Component({
selector: "app-register-new-account", selector: "app-register-new-account",
@ -23,7 +23,6 @@ export class RegisterNewAccountComponent implements OnInit {
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly accountsService: AccountsService,
private readonly store: Store, private readonly store: Store,
private readonly activatedRoute: ActivatedRoute) { private readonly activatedRoute: ActivatedRoute) {

View File

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

View File

@ -1,12 +1,9 @@
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ElementRef } from "@angular/core"; import { Component, OnInit, OnDestroy, QueryList, ViewChildren, ElementRef } from "@angular/core";
import { Stream } from "../../models/stream.models";
import { Observable, Subscription } from "rxjs"; import { Observable, Subscription } from "rxjs";
import { StreamElement } from "../../states/streams.state"; import { Select } from "@ngxs/store";
import { Store } from "@ngxs/store";
import { Http } from "@angular/http";
import { NavigationService } from "../../services/navigation.service";
import { StreamElement } from "../../states/streams.state";
import { NavigationService } from "../../services/navigation.service";
@Component({ @Component({
selector: "app-streams-main-display", selector: "app-streams-main-display",
@ -15,51 +12,29 @@ import { NavigationService } from "../../services/navigation.service";
}) })
export class StreamsMainDisplayComponent implements OnInit, OnDestroy { export class StreamsMainDisplayComponent implements OnInit, OnDestroy {
streams: Stream[] = []; @Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
private streams$: Observable<StreamElement[]>;
private streamsStateSub: Subscription;
private columnSelectedSub: Subscription; private columnSelectedSub: Subscription;
constructor( constructor(
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService) {
private readonly http: Http,
private readonly store: Store) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
} }
ngOnInit() { 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.columnSelectedSub = this.navigationService.columnSelectedSubject.subscribe((columnIndex: number) => {
this.focusOnColumn(columnIndex); this.focusOnColumn(columnIndex);
});
}); });
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.streamsStateSub.unsubscribe();
this.columnSelectedSub.unsubscribe(); this.columnSelectedSub.unsubscribe();
} }
@ViewChildren('stream', { read: ElementRef }) public streamsElementRef: QueryList<ElementRef>;; @ViewChildren('stream', { read: ElementRef }) public streamsElementRef: QueryList<ElementRef>;;
private focusOnColumn(columnIndex: number): void { private focusOnColumn(columnIndex: number): void {
console.warn(`col selected: ${columnIndex}`);
if (columnIndex > -1) { if (columnIndex > -1) {
console.warn(this.streamsElementRef);
setTimeout(() => { setTimeout(() => {
this.streamsElementRef.toArray()[columnIndex].nativeElement.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' }); 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 { export class ApiRoutes {
createApp = "/api/v1/apps"; createApp = '/api/v1/apps';
getToken = "/oauth/token"; getToken = '/oauth/token';
getAccount = "/api/v1/accounts/{0}"; getAccount = '/api/v1/accounts/{0}';
getCurrentAccount = "/api/v1/accounts/verify_credentials"; getCurrentAccount = '/api/v1/accounts/verify_credentials';
getAccountFollowers = "/api/v1/accounts/{0}/followers"; getAccountFollowers = '/api/v1/accounts/{0}/followers';
getAccountFollowing = "/api/v1/accounts/{0}/following"; getAccountFollowing = '/api/v1/accounts/{0}/following';
getAccountStatuses = "/api/v1/accounts/{0}/statuses"; getAccountStatuses = '/api/v1/accounts/{0}/statuses';
follow = "/api/v1/accounts/{0}/follow"; follow = '/api/v1/accounts/{0}/follow';
unfollow = "/api/v1/accounts/{0}/unfollow"; unfollow = '/api/v1/accounts/{0}/unfollow';
block = "/api/v1/accounts/{0}/block"; block = '/api/v1/accounts/{0}/block';
unblock = "/api/v1/accounts/{0}/unblock"; unblock = '/api/v1/accounts/{0}/unblock';
mute = "/api/v1/accounts/{0}/mute"; mute = '/api/v1/accounts/{0}/mute';
unmute = "/api/v1/accounts/{0}/unmute"; unmute = '/api/v1/accounts/{0}/unmute';
getAccountRelationships = "/api/v1/accounts/relationships"; getAccountRelationships = '/api/v1/accounts/relationships';
searchForAccounts = "/api/v1/accounts/search"; searchForAccounts = '/api/v1/accounts/search';
getBlocks = "/api/v1/blocks"; getBlocks = '/api/v1/blocks';
getFavourites = "/api/v1/favourites"; getFavourites = '/api/v1/favourites';
getFollowRequests = "/api/v1/follow_requests"; getFollowRequests = '/api/v1/follow_requests';
authorizeFollowRequest = "/api/v1/follow_requests/authorize"; authorizeFollowRequest = '/api/v1/follow_requests/authorize';
rejectFollowRequest = "/api/v1/follow_requests/reject"; rejectFollowRequest = '/api/v1/follow_requests/reject';
followRemote = "/api/v1/follows"; followRemote = '/api/v1/follows';
getInstance = "/api/v1/instance"; getInstance = '/api/v1/instance';
uploadMediaAttachment = "/api/v1/media"; uploadMediaAttachment = '/api/v1/media';
getMutes = "/api/v1/mutes"; getMutes = '/api/v1/mutes';
getNotifications = "/api/v1/notifications"; getNotifications = '/api/v1/notifications';
getSingleNotifications = "/api/v1/notifications/{0}"; getSingleNotifications = '/api/v1/notifications/{0}';
clearNotifications = "/api/v1/notifications/clear"; clearNotifications = '/api/v1/notifications/clear';
getReports = "/api/v1/reports"; getReports = '/api/v1/reports';
reportUser = "/api/v1/reports"; reportUser = '/api/v1/reports';
search = "/api/v1/search"; search = '/api/v1/search';
getStatus = "/api/v1/statuses/{0}"; getStatus = '/api/v1/statuses/{0}';
getStatusContext = "/api/v1/statuses/{0}/context"; getStatusContext = '/api/v1/statuses/{0}/context';
getStatusCard = "/api/v1/statuses/{0}/card"; getStatusCard = '/api/v1/statuses/{0}/card';
getStatusRebloggedBy = "/api/v1/statuses/{0}/reblogged_by"; getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
getStatusFavouritedBy = "/api/v1/statuses/{0}/favourited_by"; getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
postNewStatus = "/api/v1/statuses"; postNewStatus = '/api/v1/statuses';
deleteStatus = "/api/v1/statuses/{0}"; deleteStatus = '/api/v1/statuses/{0}';
reblogStatus = "/api/v1/statuses/{0}/reblog"; reblogStatus = '/api/v1/statuses/{0}/reblog';
unreblogStatus = "/api/v1/statuses/{0}/unreblog"; unreblogStatus = '/api/v1/statuses/{0}/unreblog';
favouritingStatus = "/api/v1/statuses/{0}/favourite"; favouritingStatus = '/api/v1/statuses/{0}/favourite';
unfavouritingStatus = "/api/v1/statuses/{0}/unfavourite"; unfavouritingStatus = '/api/v1/statuses/{0}/unfavourite';
getHomeTimeline = "/api/v1/timelines/home"; getHomeTimeline = '/api/v1/timelines/home';
getPublicTimeline = "/api/v1/timelines/public"; getPublicTimeline = '/api/v1/timelines/public';
getHastagTimeline = "/api/v1/timelines/tag/{0}"; 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 { 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() @Injectable()
export class StreamingService { 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 { export class StreamingWrapper {
statusUpdateSubjet = new BehaviorSubject<StatusUpdate>(null);
eventSource: WebSocket;
private apiRoutes = new ApiRoutes();
constructor(private readonly domain: string) { constructor(
const eventSource = new WebSocket(domain); private readonly mastodonService: MastodonService,
eventSource.onmessage = x => console.warn(JSON.parse(x.data)); private readonly accountInfo: AccountInfo,
eventSource.onerror = x => console.error(x); private readonly streamType: StreamTypeEnum) {
eventSource.onopen = x => console.log(x);
eventSource.onclose = x => console.log(x); 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

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