Merge pull request #4 from NicolasConstant/topic-start-column-handling
Topic start column handling
This commit is contained in:
commit
6280409a40
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ export class AccountIconComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
openMenu(event): boolean {
|
||||
openMenu(): boolean {
|
||||
this.openMenuNotify.emit(this.account);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
} */
|
|
@ -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();
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -26,9 +26,7 @@ export class StreamsSelectionFooterComponent implements OnInit {
|
|||
}
|
||||
|
||||
onColumnSelection(index: number): boolean {
|
||||
console.warn(`column selected: ${index}`);
|
||||
this.navigationService.columnSelected(index);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
} */
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}));
|
||||
});
|
|
@ -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, "");
|
||||
};
|
||||
}
|
|
@ -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}';
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ export class StreamElement {
|
|||
favorites = 4,
|
||||
activity = 5,
|
||||
list = 6,
|
||||
directmessages = 7,
|
||||
}
|
||||
directmessages = 7,
|
||||
tag = 8,
|
||||
}
|
||||
|
Loading…
Reference in New Issue