1
0
mirror of https://github.com/NicolasConstant/sengi synced 2025-02-05 21:03:54 +01:00

Merge pull request #33 from NicolasConstant/develop

merge for 0.1
This commit is contained in:
Nicolas Constant 2019-02-20 00:13:37 -05:00 committed by GitHub
commit e8a4184916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 840 additions and 305 deletions

View File

@ -10,7 +10,7 @@ let win
function createWindow() {
// Create the browser window.
win = new BrowserWindow({ width: 395, height: 800, title: "Sengi", backgroundColor: '#FFF' });
win = new BrowserWindow({ width: 393, height: 800, title: "Sengi", backgroundColor: '#FFF' });
var server = http.createServer(requestHandler).listen(9527);
win.loadURL('http://localhost:9527');

View File

@ -5,11 +5,10 @@
</app-streams-main-display>-->
<div id="display-zone">
<app-floating-column id="floating-column" *ngIf="floatingColumnActive">
</app-floating-column>
<router-outlet>
</router-outlet>
<app-tutorial id="tutorial" *ngIf="tutorialActive"></app-tutorial>
<app-floating-column id="floating-column" *ngIf="floatingColumnActive"></app-floating-column>
<app-notification-hub></app-notification-hub>
<router-outlet></router-outlet>
</div>
<app-streams-selection-footer>
@ -20,4 +19,4 @@
Welcome to {{ title }}!
</h1>
<button (click)="launchWindow()">Launch Window</button>
</div>-->
</div>-->

View File

@ -1,26 +1,33 @@
#display-zone {
position: absolute;
top: 0;
right: 0;
bottom: 30px;
left: 50px;
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
position: absolute;
top: 0;
right: 0;
bottom: 30px;
left: 50px;
overflow-y: hidden;
overflow-x: auto;
white-space: nowrap;
}
#floating-column {
top: 0;
left: 0;
bottom: 0;
z-index: 9999;
top: 0;
left: 0;
bottom: 0;
z-index: 9999;
}
#tutorial {
position: relative;
top: 0;
left: 0;
bottom: 0;
z-index: 1;
}
app-streams-selection-footer {
position: absolute;
height: 30px;
right: 0;
bottom: 0;
left: 50px;
}
position: absolute;
height: 30px;
right: 0;
bottom: 0;
left: 50px;
}

View File

@ -1,38 +1,48 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ElectronService } from 'ngx-electron';
import { NavigationService, LeftPanelType } from './services/navigation.service';
import { Subscription } from 'rxjs';
import { AccountWrapper } from './models/account.models';
import { Subscription, Observable } from 'rxjs';
import { Select } from '@ngxs/store';
// import { ElectronService } from 'ngx-electron';
import { NavigationService, LeftPanelType } from './services/navigation.service';
import { StreamElement } from './states/streams.state';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy{
title = 'app';
export class AppComponent implements OnInit, OnDestroy {
title = 'Sengi';
floatingColumnActive: boolean;
private columnEditorSub: Subscription;
floatingColumnActive: boolean;
tutorialActive: boolean;
private columnEditorSub: Subscription;
constructor(private readonly navigationService: NavigationService) {
}
ngOnInit(): void {
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
if(type === LeftPanelType.Closed) {
this.floatingColumnActive = false;
} else {
this.floatingColumnActive = true;
}
});
}
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
constructor(private readonly navigationService: NavigationService) {
}
ngOnInit(): void {
this.streamElements$.subscribe((streams: StreamElement[]) => {
if(streams && streams.length === 0){
this.tutorialActive = true;
} else {
this.tutorialActive = false;
}
});
this.columnEditorSub = this.navigationService.activatedPanelSubject.subscribe((type: LeftPanelType) => {
if (type === LeftPanelType.Closed) {
this.floatingColumnActive = false;
} else {
this.floatingColumnActive = true;
}
});
}
ngOnDestroy(): void {
this.columnEditorSub.unsubscribe();
}
ngOnDestroy(): void {
this.columnEditorSub.unsubscribe();
}
}

View File

@ -45,6 +45,9 @@ import { DatabindedTextComponent } from './components/stream/status/databinded-t
import { TimeAgoPipe } from './pipes/time-ago.pipe';
import { StreamStatusesComponent } from './components/stream/stream-statuses/stream-statuses.component';
import { StreamEditionComponent } from './components/stream/stream-edition/stream-edition.component';
import { TutorialComponent } from './components/tutorial/tutorial.component';
import { NotificationHubComponent } from './components/notification-hub/notification-hub.component';
import { NotificationService } from "./services/notification.service";
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
@ -80,7 +83,9 @@ const routes: Routes = [
DatabindedTextComponent,
TimeAgoPipe,
StreamStatusesComponent,
StreamEditionComponent
StreamEditionComponent,
TutorialComponent,
NotificationHubComponent
],
imports: [
FontAwesomeModule,
@ -98,7 +103,7 @@ const routes: Routes = [
]),
NgxsStoragePluginModule.forRoot()
],
providers: [AuthService, NavigationService, MastodonService, StreamingService],
providers: [AuthService, NavigationService, NotificationService, MastodonService, StreamingService],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

View File

@ -1,8 +1,11 @@
import { Component, OnInit, Input } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Store } from '@ngxs/store';
import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../states/registered-apps.state';
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
import { Store } from '@ngxs/store';
import { AppData } from '../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../services/notification.service';
@Component({
selector: 'app-add-new-account',
@ -13,6 +16,7 @@ export class AddNewAccountComponent implements OnInit {
@Input() mastodonFullHandle: string;
constructor(
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store) { }
@ -28,6 +32,9 @@ export class AddNewAccountComponent implements OnInit {
this.checkAndCreateApplication(instance)
.then((appData: AppData) => {
this.redirectToInstanceAuthPage(username, instance, appData);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
return false;
@ -38,10 +45,8 @@ export class AddNewAccountComponent implements OnInit {
const instanceApps = alreadyRegisteredApps.filter(x => x.instance === instance);
if (instanceApps.length !== 0) {
console.log('instance already registered');
return Promise.resolve(instanceApps[0].app);
} else {
console.log('instance not registered');
const redirect_uri = this.getLocalHostname() + '/register';
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://github.com/NicolasConstant/sengi')
.then((appData: AppData) => {

View File

@ -1,9 +1,12 @@
import { Component, OnInit, Input, ElementRef, ViewChild } from '@angular/core';
import { Store } from '@ngxs/store';
import { HttpErrorResponse } from '@angular/common/http';
import { AccountInfo } from '../../../states/accounts.state';
import { MastodonService, VisibilityEnum } from '../../../services/mastodon.service';
import { Status } from '../../../services/models/mastodon.interfaces';
import { FormsModule } from '@angular/forms';
import { NotificationService } from '../../../services/notification.service';
import { NavigationService } from '../../../services/navigation.service';
@Component({
selector: 'app-add-new-status',
@ -20,6 +23,8 @@ export class AddNewStatusComponent implements OnInit {
constructor(
private readonly store: Store,
private readonly notificationService: NotificationService,
private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService) { }
ngOnInit() {
@ -32,9 +37,6 @@ export class AddNewStatusComponent implements OnInit {
const accounts = this.getRegisteredAccounts();
const selectedAccounts = accounts.filter(x => x.isSelected);
console.warn(`selectedAccounts ${selectedAccounts.length}`);
console.warn(`statusHandle ${this.status}`);
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
switch (this.selectedPrivacy) {
case 'Public':
@ -59,9 +61,12 @@ export class AddNewStatusComponent implements OnInit {
for (const acc of selectedAccounts) {
this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler)
.then((res: Status) => {
console.log(res);
this.title = '';
this.status = '';
this.navigationService.closePanel();
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}

View File

@ -1,7 +1,8 @@
<div class="floating-column">
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()"
[browseAccountData]="overlayAccountToBrowse"
[browseHashtagData]="overlayHashtagToBrowse"></app-stream-overlay>
[browseHashtagData]="overlayHashtagToBrowse"
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
<div class="floating-column__header">
<a class="close-button" href (click)="closePanel()" title="close">x</a>
@ -12,6 +13,7 @@
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
<app-search *ngIf="openPanel === 'search'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"></app-search>
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-search>
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
</div>

View File

@ -1,7 +1,6 @@
@import "variables";
@import "mixins";
$floating-column-size: 330px;
.floating-column {
width: calc(100%);
@ -9,12 +8,14 @@ $floating-column-size: 330px;
background-color: $color-secondary;
overflow: hidden;
z-index: 99;
z-index: 200;
position: fixed;
top: 0;
bottom: $stream-selector-height;
padding: 0;
white-space: normal;
// &__header {
// }

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { NavigationService, LeftPanelType } from '../../services/navigation.service';
import { AccountWrapper } from '../../models/account.models';
import { OpenThreadEvent } from '../../services/tools.service';
@Component({
selector: 'app-floating-column',
@ -11,6 +12,7 @@ export class FloatingColumnComponent implements OnInit {
overlayActive: boolean;
overlayAccountToBrowse: string;
overlayHashtagToBrowse: string;
overlayThreadToBrowse: OpenThreadEvent;
userAccountUsed: AccountWrapper;
@ -54,18 +56,22 @@ export class FloatingColumnComponent implements OnInit {
browseAccount(account: string): void {
this.overlayAccountToBrowse = account;
this.overlayHashtagToBrowse = null;
this.overlayThreadToBrowse = null;
this.overlayActive = true;
}
browseHashtag(hashtag: string): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = hashtag;
this.overlayThreadToBrowse = null;
this.overlayActive = true;
}
browseThread(thread: string): void {
console.warn('browseThread');
console.warn(thread);
browseThread(openThreadEvent: OpenThreadEvent): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = null;
this.overlayThreadToBrowse = openThreadEvent;
this.overlayActive = true;
}
closeOverlay(): boolean {

View File

@ -4,6 +4,7 @@ import { Store } from '@ngxs/store';
import { AccountsStateModel, AccountInfo, RemoveAccount } from '../../../states/accounts.state';
import { AccountWrapper } from '../../../models/account.models';
import { NavigationService } from '../../../services/navigation.service';
import { NotificationService } from '../../../services/notification.service';
@Component({
selector: 'app-manage-account',
@ -17,7 +18,8 @@ export class ManageAccountComponent implements OnInit {
constructor(
private readonly store: Store,
private readonly navigationService: NavigationService) { }
private readonly navigationService: NavigationService,
private notificationService: NotificationService) { }
ngOnInit() {
const instance = this.account.info.instance;
@ -29,7 +31,10 @@ export class ManageAccountComponent implements OnInit {
addStream(stream: StreamElement): boolean {
if (stream) {
this.store.dispatch([new AddStream(stream)]);
this.store.dispatch([new AddStream(stream)]).toPromise()
.then(() => {
this.notificationService.notify(`${stream.displayableFullName} added`, false);
});
}
return false;
}

View File

@ -32,7 +32,8 @@
<div class="search-results__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"></app-status>
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>
</div>
</div>

View File

@ -56,7 +56,8 @@
}
}
&__status {
&__status {
font-size: 15px;
border-top: 1px solid $separator-color;
&:last-of-type {
border-bottom: 1px solid $separator-color;

View File

@ -1,13 +1,12 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Store } from '@ngxs/store';
import { HttpErrorResponse } from '@angular/common/http';
import { MastodonService } from '../../../services/mastodon.service';
import { AccountInfo } from '../../../states/accounts.state';
import { Results, Account, Status } from '../../../services/models/mastodon.interfaces';
import { ToolsService } from '../../../services/tools.service';
import { Results, Account } from '../../../services/models/mastodon.interfaces';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { StatusWrapper } from '../../stream/stream.component';
import { StreamElement, StreamTypeEnum, AddStream } from './../../../states/streams.state';
import { NotificationService } from '../../../services/notification.service';
@Component({
selector: 'app-search',
@ -25,9 +24,10 @@ export class SearchComponent implements OnInit {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
constructor(
private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService) { }
@ -47,17 +47,14 @@ export class SearchComponent implements OnInit {
return false;
}
// addHashtag(hashtag: string): boolean {
// if (hashtag) {
// const newStream = new StreamElement(StreamTypeEnum.tag, `#${hashtag}`, this.lastAccountUsed.id, hashtag, null);
// this.store.dispatch([new AddStream(newStream)]);
// }
// return false;
// }
browseThread(openThreadEvent: OpenThreadEvent): boolean{
if(openThreadEvent){
this.browseThreadEvent.next(openThreadEvent);
}
return false;
}
browseAccount(accountName: string): boolean {
console.warn(accountName);
if (accountName) {
this.browseAccountEvent.next(accountName);
}
@ -71,8 +68,6 @@ export class SearchComponent implements OnInit {
this.hashtags.length = 0;
this.isLoading = true;
console.warn(`search: ${data}`);
const enabledAccounts = this.toolsService.getSelectedAccounts();
//First candid implementation
if (enabledAccounts.length > 0) {
@ -80,7 +75,6 @@ export class SearchComponent implements OnInit {
this.mastodonService.search(this.lastAccountUsed, data, true)
.then((results: Results) => {
if (results) {
console.warn(results);
this.accounts = results.accounts.slice(0, 5);
this.hashtags = results.hashtags;
@ -88,11 +82,11 @@ export class SearchComponent implements OnInit {
const statusWrapper = new StatusWrapper(status, this.lastAccountUsed);
this.statuses.push(statusWrapper);
}
}
})
.catch((err) => console.error(err))
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
})
.then(() => { this.isLoading = false; });
}
}

View File

@ -1,8 +1,9 @@
<div class="left-bar">
<a class="left-bar-button left-bar-button--status left-bar-link" href title="write new message" (click)="createNewStatus()">
<ion-icon name="md-send"></ion-icon>
<a class="left-bar-button left-bar-button--status left-bar-link" href title="write new message" (click)="createNewStatus()" *ngIf="hasAccounts">
<fa-icon [icon]="faCommentAlt"></fa-icon>
<!-- <ion-icon name="md-send"></ion-icon> -->
</a>
<a class="left-bar-button left-bar-button--search left-bar-link" href title="search" (click)="openSearch()">
<a class="left-bar-button left-bar-button--search left-bar-link" href title="search" (click)="openSearch()" *ngIf="hasAccounts">
<ion-icon name="md-search"></ion-icon>
</a>
@ -11,11 +12,11 @@
</app-account-icon>
</div>
<a class="left-bar-button left-bar-button--add left-bar-link" href title="add new account" (click)="addNewAccount()">
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }" href title="add new account" (click)="addNewAccount()">
<ion-icon name="md-add"></ion-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-link" href title="settings" (click)="openSettings()">
<a class="left-bar-button left-bar-button--cog left-bar-link" href title="settings" (click)="openSettings()" *ngIf="hasAccounts">
<ion-icon name="md-cog"></ion-icon>
</a>
</div>

View File

@ -1,6 +1,7 @@
@import "variables";
$width-button: 50px;
$height-button: 40px;
.left-bar {
width: $width-button;
height: calc(100%);
@ -22,12 +23,16 @@ $height-button: 40px;
width: $width-button;
height: $height-button;
transition: all .2s;
// outline: 1px dotted greenyellow;
&--status {
padding: 5px 0 0 10px;
//margin-top: 3px;
font-size: 26px;
padding: 8px 0 2px 12px;
}
&--search {
padding: 0 0 0 9px;
font-size: 28px;
padding: 0 0 0 11px;
}
&--add {
padding: 0 0 0 12px;
@ -62,3 +67,7 @@ $height-button: 40px;
}
}
.no-accounts {
padding-top: 10px;
// color: cornflowerblue;
}

View File

@ -1,13 +1,15 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription, BehaviorSubject, Observable } from "rxjs";
import { HttpErrorResponse } from "@angular/common/http";
import { Subscription, Observable } from "rxjs";
import { Store } from "@ngxs/store";
import { faCommentAlt } from "@fortawesome/free-regular-svg-icons";
import { Account } from "../../services/models/mastodon.interfaces";
import { AccountWrapper } from "../../models/account.models";
import { AccountsStateModel, AccountInfo, SelectAccount } from "../../states/accounts.state";
import { AccountInfo, SelectAccount } from "../../states/accounts.state";
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
import { MastodonService } from "../../services/mastodon.service";
import { NotificationService } from "../../services/notification.service";
@Component({
selector: "app-left-side-bar",
@ -15,13 +17,17 @@ import { MastodonService } from "../../services/mastodon.service";
styleUrls: ["./left-side-bar.component.scss"]
})
export class LeftSideBarComponent implements OnInit, OnDestroy {
faCommentAlt = faCommentAlt;
accounts: AccountWrapper[] = [];
hasAccounts: boolean;
private accounts$: Observable<AccountInfo[]>;
// private loadedAccounts: { [index: string]: AccountInfo } = {};
private sub: Subscription;
constructor(
private readonly notificationService: NotificationService,
private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService,
private readonly store: Store) {
@ -46,6 +52,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.mastodonService.retrieveAccountDetails(acc)
.then((result: Account) => {
accWrapper.avatar = result.avatar;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
}
@ -55,6 +64,8 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
for(let delAcc of deletedAccounts){
this.accounts = this.accounts.filter(x => x.info.id !== delAcc.info.id);
}
this.hasAccounts = this.accounts.length > 0;
}
});
}
@ -64,12 +75,10 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
}
onToogleAccountNotify(acc: AccountWrapper) {
console.warn(`onToogleAccountNotify username ${acc.info.username}`);
this.store.dispatch([new SelectAccount(acc.info)]);
}
onOpenMenuNotify(acc: AccountWrapper) {
console.warn(`onOpenMenuNotify username ${acc.info.username}`);
this.navigationService.openColumnEditor(acc);
}

View File

@ -0,0 +1,5 @@
<div class="notification-hub">
<div class="notification-hub__notification" [ngClass]="{'notification-hub__notification--error':notification.isError}" *ngFor="let notification of notifications" (click)="onClick(notification)" title="close">
{{ notification.message }}
</div>
</div>

View File

@ -0,0 +1,20 @@
.notification-hub {
position: fixed;
bottom: 30px;
z-index: 9999999;
margin: 0 0 10px 0;
&__notification{
background-color: #22b90e;
color: black;
padding: 5px 10px;
border-radius: 2px;
margin: 0 0 5px 15px;
cursor: pointer;
&--error{
background-color: #be0a0a;
color: whitesmoke;
}
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NotificationHubComponent } from './notification-hub.component';
xdescribe('NotificationHubComponent', () => {
let component: NotificationHubComponent;
let fixture: ComponentFixture<NotificationHubComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NotificationHubComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NotificationHubComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService, NotificatioData } from '../../services/notification.service';
@Component({
selector: 'app-notification-hub',
templateUrl: './notification-hub.component.html',
styleUrls: ['./notification-hub.component.scss']
})
export class NotificationHubComponent implements OnInit {
notifications: NotificatioData[] = [];
constructor(private notificationService: NotificationService) { }
ngOnInit() {
this.notificationService.notifactionStream.subscribe((notification: NotificatioData) => {
this.notifications.push(notification);
setTimeout(() => {
this.notifications = this.notifications.filter(x => x.id !== notification.id);
}, 2000);
});
//this.autoSubmit();
}
autoSubmit(): any {
this.notificationService.notify("test message", true);
setTimeout(() => {
this.autoSubmit();
}, 1500);
}
onClick(notification: NotificatioData): void{
this.notifications = this.notifications.filter(x => x.id !== notification.id);
}
}

View File

@ -6,9 +6,10 @@
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board">add column</button>
</a>
</div>
<app-stream-statuses class="hashtag-stream" *ngIf="hashtagElement"
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
[streamElement]="hashtagElement"
[goToTop]="goToTopSubject.asObservable()"
[userLocked]="false"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>

View File

@ -1,8 +1,11 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { Component, OnInit, Output, EventEmitter, Input, ViewChild } from '@angular/core';
import { Subject } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
import { AccountInfo } from '../../../states/accounts.state';
@Component({
selector: 'app-hashtag',
@ -10,16 +13,30 @@ import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/stream
styleUrls: ['./hashtag.component.scss']
})
export class HashtagComponent implements OnInit {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input() hashtagElement: StreamElement;
private _hashtagElement: StreamElement;
@Input()
set hashtagElement(hashtagElement: StreamElement){
this._hashtagElement = hashtagElement;
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
}
get hashtagElement(): StreamElement{
return this._hashtagElement;
}
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
goToTopSubject: Subject<void> = new Subject<void>();
private lastUsedAccount: AccountInfo;
constructor(
private readonly store: Store) { }
private readonly store: Store,
private readonly toolsService: ToolsService) { }
ngOnInit() {
}
@ -33,12 +50,17 @@ export class HashtagComponent implements OnInit {
event.stopPropagation();
const hashtag = this.hashtagElement.tag;
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.hashtagElement.accountId, hashtag, null, this.hashtagElement.displayableFullName);
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.lastUsedAccount.id, hashtag, null, this.hashtagElement.displayableFullName);
this.store.dispatch([new AddStream(newStream)]);
return false;
}
refresh(): any {
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.appStreamStatuses.refresh();
}
browseAccount(account: string) {
this.browseAccountEvent.next(account);
}
@ -47,7 +69,7 @@ export class HashtagComponent implements OnInit {
this.browseHashtagEvent.next(hashtag);
}
browseThread(statusUri: string): void {
this.browseThreadEvent.next(statusUri);
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Store } from '@ngxs/store';
import { Observable, Subscription } from 'rxjs';
@ -6,6 +7,8 @@ import { StatusWrapper } from '../../stream.component';
import { MastodonService } from '../../../../services/mastodon.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { Status, Results } from '../../../../services/models/mastodon.interfaces';
import { ToolsService } from '../../../../services/tools.service';
import { NotificationService } from '../../../../services/notification.service';
// import { map } from "rxjs/operators";
@Component({
@ -35,15 +38,14 @@ export class ActionBarComponent implements OnInit, OnDestroy {
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService) {
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
// const selectedAccounts = this.getSelectedAccounts();
// this.checkStatus(selectedAccounts);
const status = this.statusWrapper.status;
const account = this.statusWrapper.provider;
this.favoriteStatePerAccountId[account.id] = status.favourited;
@ -86,23 +88,11 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
boost(): boolean {
//TODO get rid of that
this.selectedAccounts.forEach((account: AccountInfo) => {
const isProvider = this.statusWrapper.provider.id === account.id;
let pipeline: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (!isProvider) {
pipeline = pipeline.then((foreignStatus: Status) => {
const statusUrl = foreignStatus.url;
return this.mastodonService.search(account, statusUrl)
.then((results: Results) => {
//TODO check and type errors
return results.statuses[0];
});
});
}
pipeline
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isBoosted) {
return this.mastodonService.unreblog(account, status);
@ -115,8 +105,8 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.checkIfBoosted();
// this.isBoosted = !this.isBoosted;
})
.catch(err => {
console.error(err);
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
});
@ -125,22 +115,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
favorite(): boolean {
this.selectedAccounts.forEach((account: AccountInfo) => {
const isProvider = this.statusWrapper.provider.id === account.id;
let pipeline: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (!isProvider) {
pipeline = pipeline.then((foreignStatus: Status) => {
const statusUrl = foreignStatus.url;
return this.mastodonService.search(account, statusUrl)
.then((results: Results) => {
//TODO check and type errors
return results.statuses[0];
});
});
}
pipeline
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
usableStatus
.then((status: Status) => {
if (this.isFavorited) {
return this.mastodonService.unfavorite(account, status);
@ -153,8 +130,8 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.checkIfFavorited();
// this.isFavorited = !this.isFavorited;
})
.catch(err => {
console.error(err);
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
});
return false;
@ -180,7 +157,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
more(): boolean {
console.warn('more');
console.warn('more'); //TODO
return false;
}

View File

@ -5,6 +5,8 @@ import { MastodonService, VisibilityEnum } from '../../../../services/mastodon.s
import { StatusWrapper } from '../../stream.component';
import { Status } from '../../../../services/models/mastodon.interfaces';
import { ToolsService } from '../../../../services/tools.service';
import { NotificationService } from '../../../../services/notification.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-reply-to-status',
@ -24,25 +26,28 @@ export class ReplyToStatusComponent implements OnInit {
constructor(
// private readonly store: Store,
private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService) { }
ngOnInit() {
this.statusReplyingTo = this.statusReplyingToWrapper.status;
if (this.statusReplyingToWrapper.status.reblog) {
this.statusReplyingTo = this.statusReplyingToWrapper.status.reblog;
} else {
this.statusReplyingTo = this.statusReplyingToWrapper.status;
}
this.status += `@${this.statusReplyingTo.account.acct} `;
for (const mention of this.statusReplyingTo.mentions) {
this.status += `@${mention.acct} `;
}
setTimeout(() => {
setTimeout(() => {
this.replyElement.nativeElement.focus();
}, 0);
}
onSubmit(): boolean {
const selectedAccounts = this.toolsService.getSelectedAccounts();
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
switch (this.selectedPrivacy) {
case 'Public':
@ -61,12 +66,20 @@ export class ReplyToStatusComponent implements OnInit {
let spoiler = this.statusReplyingTo.spoiler_text;
const selectedAccounts = this.toolsService.getSelectedAccounts();
for (const acc of selectedAccounts) {
this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler, this.statusReplyingTo.id)
const usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
usableStatus
.then((status: Status) => {
return this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler, status.id);
})
.then((res: Status) => {
console.log(res);
this.status = '';
this.onClose.emit();
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}

View File

@ -1,6 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";
import { Status, Account } from "../../../services/models/mastodon.interfaces";
import { StatusWrapper } from "../stream.component";
import { OpenThreadEvent } from "../../../services/tools.service";
@Component({
selector: "app-status",
@ -15,7 +16,7 @@ export class StatusComponent implements OnInit {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private _statusWrapper: StatusWrapper;
status: Status;
@ -78,11 +79,15 @@ export class StatusComponent implements OnInit {
textSelected(): void {
const status = this._statusWrapper.status;
const accountInfo = this._statusWrapper.provider;
if (status.reblog) {
this.browseThreadEvent.next(status.reblog.uri);
let openThread: OpenThreadEvent;
if (status.reblog) {
openThread = new OpenThreadEvent(status.reblog, accountInfo);
} else {
this.browseThreadEvent.next(this._statusWrapper.status.uri);
openThread = new OpenThreadEvent(status, accountInfo);
}
this.browseThreadEvent.next(openThread);
}
}

View File

@ -6,15 +6,15 @@
<a href class="overlay-next" *ngIf="canGoForward" (click)="next()">NEXT</a>
</div>
<app-user-profile *ngIf="accountName" [currentAccount]="accountName"
<app-user-profile #appUserProfile *ngIf="accountName" [currentAccount]="accountName"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-user-profile>
<app-hashtag *ngIf="hashtagElement" [hashtagElement]="hashtagElement"
<app-hashtag #appHashtag *ngIf="hashtagElement" [hashtagElement]="hashtagElement"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-hashtag>
<app-thread *ngIf="browseThread" [currentThread]="thread"
<app-thread #appThread *ngIf="browseThread" [currentThread]="thread"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-thread>

View File

@ -1,8 +1,10 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { Account, Results } from "../../../services/models/mastodon.interfaces";
import { MastodonService } from '../../../services/mastodon.service';
import { ToolsService } from '../../../services/tools.service';
import { Component, OnInit, Output, EventEmitter, Input, ViewChild } from '@angular/core';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
import { ThreadComponent } from '../thread/thread.component';
import { UserProfileComponent } from '../user-profile/user-profile.component';
import { HashtagComponent } from '../hashtag/hashtag.component';
@Component({
selector: 'app-stream-overlay',
@ -15,11 +17,11 @@ export class StreamOverlayComponent implements OnInit {
private nextElements: OverlayBrowsing[] = [];
private currentElement: OverlayBrowsing;
canRefresh: boolean;
canRefresh: boolean = true;
canGoForward: boolean;
accountName: string;
thread: string;
thread: OpenThreadEvent;
// hashtag: string;
hashtagElement: StreamElement;
@ -32,8 +34,8 @@ export class StreamOverlayComponent implements OnInit {
}
@Input('browseThreadData')
set browseThreadData(statusUri: string) {
this.browseThread(statusUri);
set browseThreadData(openThread: OpenThreadEvent) {
this.browseThread(openThread);
}
@Input('browseHashtagData')
@ -41,7 +43,11 @@ export class StreamOverlayComponent implements OnInit {
this.browseHashtag(hashtag);
}
constructor(private toolsService: ToolsService) { }
@ViewChild('appUserProfile') appUserProfile: UserProfileComponent;
@ViewChild('appHashtag') appHashtag: HashtagComponent;
@ViewChild('appThread') appThread: ThreadComponent;
constructor(private readonly toolsService: ToolsService) { }
ngOnInit() {
}
@ -52,8 +58,6 @@ export class StreamOverlayComponent implements OnInit {
}
next(): boolean {
console.log('next');
if (this.nextElements.length === 0) {
return false;
}
@ -70,8 +74,6 @@ export class StreamOverlayComponent implements OnInit {
}
previous(): boolean {
console.log('previous');
if (this.previousElements.length === 0) {
this.closeOverlay.next();
return false;
@ -89,14 +91,20 @@ export class StreamOverlayComponent implements OnInit {
}
refresh(): boolean {
console.log('refresh');
if(this.thread){
this.appThread.refresh();
} else if(this.hashtagElement){
this.appHashtag.refresh();
} else if(this.accountName){
this.appUserProfile.refresh();
}
return false;
}
browseAccount(accountName: string): void {
if(!accountName) return;
console.log('accountSelected');
this.nextElements.length = 0;
if (this.currentElement) {
this.previousElements.push(this.currentElement);
@ -109,7 +117,6 @@ export class StreamOverlayComponent implements OnInit {
browseHashtag(hashtag: string): void {
if(!hashtag) return;
console.log('hashtagSelected');
this.nextElements.length = 0;
if (this.currentElement) {
this.previousElements.push(this.currentElement);
@ -122,16 +129,15 @@ export class StreamOverlayComponent implements OnInit {
this.canGoForward = false;
}
browseThread(statusUri: string): any {
if(!statusUri) return;
browseThread(openThread: OpenThreadEvent): any {
if(!openThread) return;
console.log('thread selected')
this.nextElements.length = 0;
if (this.currentElement) {
this.previousElements.push(this.currentElement);
}
const newElement = new OverlayBrowsing(null, null, statusUri);
const newElement = new OverlayBrowsing(null, null, openThread);
this.loadElement(newElement);
this.canGoForward = false;
}
@ -149,9 +155,7 @@ class OverlayBrowsing {
constructor(
public readonly hashtag: StreamElement,
public readonly account: string,
public readonly thread: string) {
console.warn(`OverlayBrowsing: ${hashtag} ${account} ${thread}`);
public readonly thread: OpenThreadEvent) {
if (hashtag) {
this.type = OverlayEnum.hashtag;

View File

@ -1,9 +1,13 @@
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
<!-- data-simplebar -->
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" (browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
<app-status [statusWrapper]="statusWrapper"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>
</div>

View File

@ -6,6 +6,12 @@
width: calc(100%);
overflow: auto;
&__error {
padding: 20px 20px 0 20px;
color: rgb(255, 113, 113);
}
&__status:not(:last-child) {
border: solid #06070b;
border-width: 0 0 1px 0;

View File

@ -1,4 +1,6 @@
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, EventEmitter, Output } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement } from '../../../states/streams.state';
@ -6,9 +8,9 @@ import { AccountInfo } from '../../../states/accounts.state';
import { StreamingService, EventEnum, StreamingWrapper, StatusUpdate } from '../../../services/streaming.service';
import { Status } from '../../../services/models/mastodon.interfaces';
import { MastodonService } from '../../../services/mastodon.service';
import { Observable, Subscription } from 'rxjs';
import { StatusWrapper } from '../stream.component';
import { NotificationService } from '../../../services/notification.service';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
@Component({
selector: 'app-stream-statuses',
@ -16,7 +18,9 @@ import { StatusWrapper } from '../stream.component';
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent implements OnInit, OnDestroy {
isLoading = false; //TODO
displayError: string;
private _streamElement: StreamElement;
private account: AccountInfo;
@ -28,22 +32,12 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input()
set streamElement(streamElement: StreamElement) {
console.warn('new stream');
this.resetStream();
this._streamElement = streamElement;
const splitedUserName = streamElement.accountId.split('@');
const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
this.retrieveToots();
this.launchWebsocket();
this.load(this._streamElement);
}
get streamElement(): StreamElement {
return this._streamElement;
@ -51,10 +45,14 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
@Input() goToTop: Observable<void>;
@Input() userLocked = true;
private goToTopSubscription: Subscription;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly streamingService: StreamingService,
private readonly mastodonService: MastodonService) {
}
@ -65,14 +63,34 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
});
}
ngOnDestroy(){
if( this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
ngOnDestroy() {
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
}
refresh(): any {
this.load(this._streamElement);
}
private load(streamElement: StreamElement) {
this.resetStream();
if (this.userLocked) {
const splitedUserName = streamElement.accountId.split('@');
const user = splitedUserName[0];
const instance = splitedUserName[1];
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
} else {
this.account = this.toolsService.getSelectedAccounts()[0];
}
this.retrieveToots();
this.launchWebsocket();
}
private resetStream() {
this.statuses.length = 0;
this.bufferStream.length = 0;
if(this.websocketStreaming) this.websocketStreaming.dispose();
if (this.websocketStreaming) this.websocketStreaming.dispose();
}
private launchWebsocket(): void {
@ -95,7 +113,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
});
}
@ViewChild('statusstream') public statustream: ElementRef;
private applyGoToTop(): boolean {
this.loadBuffer();
@ -115,7 +133,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
onScroll() {
var element = this.statustream.nativeElement as HTMLElement;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
const atTop = element.scrollTop === 0;
this.streamPositionnedAtTop = false;
@ -134,12 +152,12 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.browseHashtagEvent.next(hashtag);
}
browseThread(statusUri: string): void {
this.browseThreadEvent.next(statusUri);
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
textSelected(): void {
console.warn(`status comp: textSelected`);
console.warn(`status comp: textSelected`); //TODO
}
private scrolledToTop() {
@ -148,15 +166,15 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.loadBuffer();
}
private loadBuffer(){
if(this.bufferWasCleared) {
private loadBuffer() {
if (this.bufferWasCleared) {
this.statuses.length = 0;
this.bufferWasCleared = false;
}
for (const status of this.bufferStream) {
const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper);
this.statuses.unshift(wrapper);
}
this.bufferStream.length = 0;
@ -173,8 +191,8 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
this.statuses.push(wrapper);
}
})
.catch(err => {
console.error(err);
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.isProcessingInfiniteScroll = false;
@ -186,7 +204,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
return regAccounts;
}
private retrieveToots(): void {
this.mastodonService.getTimeline(this.account, this._streamElement.type, null, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.list)
.then((results: Status[]) => {
@ -194,9 +212,12 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;

View File

@ -1,7 +1,9 @@
<div class="stream-column">
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()"
[browseAccountData]="overlayAccountToBrowse" [browseHashtagData]="overlayHashtagToBrowse" [browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
[browseAccountData]="overlayAccountToBrowse"
[browseHashtagData]="overlayHashtagToBrowse"
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
<div class="stream-column__stream-header">
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">

View File

@ -5,6 +5,7 @@ import { faHome, faGlobe, faUser, faHashtag, faListUl, faBars, IconDefinition }
import { StreamElement, StreamTypeEnum } from "../../states/streams.state";
import { Status } from "../../services/models/mastodon.interfaces";
import { AccountInfo } from "../../states/accounts.state";
import { OpenThreadEvent } from "../../services/tools.service";
@Component({
selector: "app-stream",
@ -18,7 +19,7 @@ export class StreamComponent implements OnInit {
overlayActive: boolean;
overlayAccountToBrowse: string;
overlayHashtagToBrowse: string;
overlayThreadToBrowse: string;
overlayThreadToBrowse: OpenThreadEvent;
goToTopSubject: Subject<void> = new Subject<void>();
@ -75,10 +76,10 @@ export class StreamComponent implements OnInit {
this.overlayActive = true;
}
browseThread(statusUri: string): void {
browseThread(openThreadEvent: OpenThreadEvent): void {
this.overlayAccountToBrowse = null;
this.overlayHashtagToBrowse = null;
this.overlayThreadToBrowse = statusUri;
this.overlayThreadToBrowse = openThreadEvent;
this.overlayActive = true;
}
@ -91,7 +92,6 @@ export class StreamComponent implements OnInit {
editionPanelIsOpen: boolean;
openEditionMenu(): boolean {
console.log('opened menu');
this.editionPanelIsOpen = !this.editionPanelIsOpen;
return false;
}

View File

@ -1,64 +1,107 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { StatusWrapper } from '../stream.component';
import { MastodonService } from '../../../services/mastodon.service';
import { ToolsService } from '../../../services/tools.service';
import { Status, Results, Context } from '../../../services/models/mastodon.interfaces';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { Results, Context, Status } from '../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../services/notification.service';
import { AccountInfo } from '../../../states/accounts.state';
@Component({
selector: 'app-thread',
templateUrl: '../stream-statuses/stream-statuses.component.html',
styleUrls: ['../stream-statuses/stream-statuses.component.scss']
})
export class ThreadComponent implements OnInit {
export class ThreadComponent implements OnInit {
statuses: StatusWrapper[] = [];
isLoading: boolean;
displayError: string;
private lastThreadEvent: OpenThreadEvent;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('currentThread')
set currentThread(thread: string) {
set currentThread(thread: OpenThreadEvent) {
if (thread) {
this.isLoading = true;
this.lastThreadEvent = thread;
this.getThread(thread);
}
}
constructor(
private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService) { }
ngOnInit() {
}
private getThread(thread: string) {
private getThread(openThreadEvent: OpenThreadEvent) {
this.statuses.length = 0;
let currentAccount = this.toolsService.getSelectedAccounts()[0];
this.mastodonService.search(currentAccount, thread, true)
.then((result: Results) => {
if (result.statuses.length === 1) {
const retrievedStatus = result.statuses[0];
this.mastodonService.getStatusContext(currentAccount, retrievedStatus.id)
.then((context: Context) => {
this.isLoading = false;
let contextStatuses = [...context.ancestors, retrievedStatus, ...context.descendants]
const status = openThreadEvent.status;
const sourceAccount = openThreadEvent.sourceAccount;
for (const s of contextStatuses) {
const wrapper = new StatusWrapper(s, currentAccount);
this.statuses.push(wrapper);
}
});
} else {
//TODO handle error
this.isLoading = false;
console.error('could not retrieve status');
}
if (status.visibility === 'public' || status.visibility === 'unlisted') {
var statusPromise: Promise<Status> = Promise.resolve(status);
if (sourceAccount.id !== currentAccount.id) {
statusPromise = this.mastodonService.search(currentAccount, status.uri, true)
.then((result: Results) => {
if (result.statuses.length === 1) {
const retrievedStatus = result.statuses[0];
return retrievedStatus;
}
throw new Error('could not find status');
});
}
this.retrieveThread(currentAccount, statusPromise);
} else if (sourceAccount.id === currentAccount.id) {
var statusPromise = Promise.resolve(status);
this.retrieveThread(currentAccount, statusPromise);
} else {
this.isLoading = false;
this.displayError = `You need to use your account ${sourceAccount.username}@${sourceAccount.instance} to show this thread`;
}
}
private retrieveThread(currentAccount: AccountInfo, pipeline: Promise<Status>) {
pipeline
.then((status: Status) => {
this.mastodonService.getStatusContext(currentAccount, status.id)
.then((context: Context) => {
this.isLoading = false;
let contextStatuses = [...context.ancestors, status, ...context.descendants]
for (const s of contextStatuses) {
const wrapper = new StatusWrapper(s, currentAccount);
this.statuses.push(wrapper);
}
})
.catch((err: HttpErrorResponse) => {
this.isLoading = false;
this.notificationService.notifyHttpError(err);
});
});
}
refresh(): any {
this.isLoading = true;
this.statuses.length = 0;
this.getThread(this.lastThreadEvent);
}
onScroll() {
//Do nothing
}
@ -71,7 +114,7 @@ export class ThreadComponent implements OnInit {
this.browseHashtagEvent.next(hashtag);
}
browseThread(statusUri: string): void {
this.browseThreadEvent.next(statusUri);
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
}

View File

@ -1,16 +1,16 @@
<div class="profile">
<div class="profile ">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
<div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
</div>
</div>
<div class="profile-sub-header flexcroll">
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
<div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
</div>
</div>
<div *ngIf="account && hasNote" class="profile-description">
<app-databinded-text class="status__content" [textIsSelectable]="false" [text]="account.note"
(accountSelected)="browseAccount($event)"

View File

@ -44,6 +44,7 @@ $header-height: 160px;
&-sub-header {
overflow: auto;
height: calc(100% - #{$header-height});
height: calc(100%);
// height: calc(20% - 190px);
// height: 150px;
// border: 1px solid greenyellow;

View File

@ -1,8 +1,11 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Account, Status, Results } from "../../../services/models/mastodon.interfaces";
import { HttpErrorResponse } from '@angular/common/http';
import { Account, Status } from "../../../services/models/mastodon.interfaces";
import { MastodonService } from '../../../services/mastodon.service';
import { ToolsService } from '../../../services/tools.service';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { StatusWrapper } from '../stream.component';
import { NotificationService } from '../../../services/notification.service';
@Component({
selector: 'app-user-profile',
@ -10,6 +13,7 @@ import { StatusWrapper } from '../stream.component';
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
account: Account;
hasNote: boolean;
@ -19,15 +23,28 @@ export class UserProfileComponent implements OnInit {
statuses: StatusWrapper[] = [];
private accountName: string;
private lastAccountName: string;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('currentAccount')
//set currentAccount(account: Account) {
set currentAccount(accountName: string) {
this.lastAccountName = accountName;
this.load(this.lastAccountName);
}
constructor(
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
ngOnInit() {
}
private load(accountName: string) {
this.statuses.length = 0;
this.isLoading = true;
@ -37,19 +54,17 @@ export class UserProfileComponent implements OnInit {
this.hasNote = account && account.note && account.note !== '<p></p>';
return this.getStatuses(this.account);
})
.catch(err => {
this.error = 'Error when retrieving account';
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
})
.then(() => {
this.isLoading = false;
this.statusLoading = false;
console.error(this.error);
});
}
constructor(
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
ngOnInit() {
refresh(): any {
this.load(this.lastAccountName);
}
browseAccount(accountName: string): void {
@ -60,13 +75,13 @@ export class UserProfileComponent implements OnInit {
this.browseHashtagEvent.next(hashtag);
}
browseThread(statusUri: string): void {
this.browseThreadEvent.next(statusUri);
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
private loadAccount(accountName: string): Promise<Account> {
this.account = null;
this.accountName = accountName;
let selectedAccounts = this.toolsService.getSelectedAccounts();
if (selectedAccounts.length === 0) {
@ -96,11 +111,5 @@ export class UserProfileComponent implements OnInit {
}
this.statusLoading = false;
});
// .catch(err => {
// })
// .then(() => {
// this.statusLoading = false;
// });
}
}

View File

@ -0,0 +1,19 @@
<!-- <div class="tutorial"> -->
<div class="add-account" *ngIf="showAddAccount">
<!-- <div class="add-account__arrow"></div> -->
<img class="add-account__arrow" src="assets/img/arrow_1.png" alt="arrow pointing the +">
<h3 class="add-account__title">Welcome to Sengi!</h3>
<p class="add-account__description">
Let's start, click the "+" button to add a new account.
</p>
</div>
<div class="open-account" *ngIf="showOpenAccount">
<!-- <div class="open-account__arrow"></div> -->
<img class="open-account__arrow" src="assets/img/arrow_2.png" alt="arrow pointing the first account">
<div class="open-account__mouse-icon"></div>
<h3 class="open-account__title">Nice!</h3>
<p class="open-account__description">
Now <span class="underline">left-click</span> on your avatar to open your account and be able to add some timelines!
</p>
</div>
<!-- </div> -->

View File

@ -0,0 +1,83 @@
@import "variables";
@import "mixins";
// .tutorial {
// width: $floating-column-size;
// overflow: hidden;
// z-index: 99;
// position: fixed;
// top: 0;
// bottom: $stream-selector-height;
// padding: 0;
// font-size: $default-font-size;
// }
.underline {
text-decoration: underline;
}
.add-account{
position: absolute;
&__arrow {
position: fixed;
top: 15px;
left: 60px;
}
&__title{
position: relative;
top: 30px;
left: 70px;
}
&__description {
position: relative;
top: 45px;
left: 75px;
text-align: center;
width: 200px;
display: inline-block;
//word-break: break-all;
white-space: normal;
}
}
.open-account{
position: absolute;
&__arrow {
position: fixed;
top: 85px;
left: 65px;
}
&__title{
position: relative;
top: 30px;
left: 160px;
width: 50px;
}
&__description {
position: relative;
top: 40px;
left: 90px;
text-align: right;
width: 200px;
display: inline-block;
// word-break: break-all;
white-space: normal;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TutorialComponent } from './tutorial.component';
xdescribe('TutorialComponent', () => {
let component: TutorialComponent;
let fixture: ComponentFixture<TutorialComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TutorialComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TutorialComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,62 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Select } from '@ngxs/store';
import { Observable, Subscription } from 'rxjs';
import { AccountInfo } from '../../states/accounts.state';
import { StreamElement } from '../../states/streams.state';
@Component({
selector: 'app-tutorial',
templateUrl: './tutorial.component.html',
styleUrls: ['./tutorial.component.scss']
})
export class TutorialComponent implements OnInit, OnDestroy {
public showAddAccount: boolean;
public showOpenAccount: boolean;
private hasAccounts: boolean;
private hasColumns: boolean;
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
@Select(state => state.registeredaccounts.accounts) accounts$: Observable<AccountInfo[]>;
private accountsSub: Subscription;
private steamsSub: Subscription;
constructor() {
}
ngOnInit() {
this.accountsSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (accounts) {
if (accounts.length === 0) {
this.showAddAccount = true;
this.showOpenAccount = false;
} else {
this.hasAccounts = true;
this.showAddAccount = false;
if (!this.hasColumns) {
this.showOpenAccount = true;
}
}
}
});
this.steamsSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
if (streams) {
if (streams.length === 0 && this.hasAccounts) {
this.showOpenAccount = true;
} else if(streams.length > 0 && this.hasAccounts){
this.hasColumns = true;
this.showOpenAccount = false;
}
}
});
}
ngOnDestroy(): void {
this.accountsSub.unsubscribe();
this.steamsSub.unsubscribe();
}
}

View File

@ -1,13 +1,13 @@
import { Component, OnInit, Input } from "@angular/core";
import { Store, Select } from '@ngxs/store';
import { ActivatedRoute, Router } from "@angular/router";
import { Observable } from "rxjs";
import { HttpErrorResponse } from "@angular/common/http";
import { AuthService, CurrentAuthProcess } from "../../services/auth.service";
import { TokenData, AppData } from "../../services/models/mastodon.interfaces";
import { AddRegisteredApp, RegisteredAppsState, RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { TokenData } from "../../services/models/mastodon.interfaces";
import { RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { AccountInfo, AddAccount } from "../../states/accounts.state";
import { MastodonService } from "../../services/mastodon.service";
import { NotificationService } from "../../services/notification.service";
@Component({
selector: "app-register-new-account",
@ -23,6 +23,7 @@ export class RegisterNewAccountComponent implements OnInit {
private authStorageKey: string = 'tempAuth';
constructor(
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store,
private readonly activatedRoute: ActivatedRoute,
@ -57,6 +58,9 @@ export class RegisterNewAccountComponent implements OnInit {
localStorage.removeItem(this.authStorageKey);
this.router.navigate(['/home']);
});
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
});
}

View File

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

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable()
export class NotificationService {
public notifactionStream = new Subject<NotificatioData>();
constructor() {
}
public notify(message: string, isError: boolean){
let newNotification = new NotificatioData(message, isError);
this.notifactionStream.next(newNotification);
}
public notifyHttpError(err: HttpErrorResponse){
console.error(err.message);
let message = `${err.status}: ${err.statusText}`;
this.notify(message, true);
}
}
export class NotificatioData {
public id: string;
constructor(
public message: string,
public isError: boolean
) {
this.id = `${message}${new Date().getTime()}`;
}
}

View File

@ -3,7 +3,8 @@ import { Store } from '@ngxs/store';
import { AccountInfo } from '../states/accounts.state';
import { MastodonService } from './mastodon.service';
import { Account, Results } from "./models/mastodon.interfaces";
import { Account, Results, Status } from "./models/mastodon.interfaces";
import { StatusWrapper } from '../components/stream/stream.component';
@Injectable({
@ -24,10 +25,6 @@ export class ToolsService {
findAccount(account: AccountInfo, accountName: string): Promise<Account> {
return this.mastodonService.search(account, accountName, true)
.then((result: Results) => {
console.warn('findAccount');
console.warn(`accountName ${accountName}`);
console.warn(result);
if(accountName[0] === '@') accountName = accountName.substr(1);
const foundAccount = result.accounts.filter(
@ -38,4 +35,29 @@ export class ToolsService {
});
}
getStatusUsableByAccount(account: AccountInfo, originalStatus: StatusWrapper): Promise<Status>{
const isProvider = originalStatus.provider.id === account.id;
let statusPromise: Promise<Status> = Promise.resolve(originalStatus.status);
if (!isProvider) {
statusPromise = statusPromise.then((foreignStatus: Status) => {
const statusUrl = foreignStatus.url;
return this.mastodonService.search(account, statusUrl)
.then((results: Results) => {
return results.statuses[0];
});
});
}
return statusPromise;
}
}
export class OpenThreadEvent {
constructor(
public status: Status,
public sourceAccount: AccountInfo
) {
}
}

View File

@ -29,10 +29,13 @@ export interface AccountsStateModel {
export class AccountsState {
@Action(AddAccount)
AddAccount(ctx: StateContext<AccountsStateModel>, action: AddAccount) {
const state = ctx.getState();
const newAcc = action.account;
newAcc.id = `${newAcc.username}@${newAcc.instance}`;
const state = ctx.getState();
if(state.accounts.filter(x => x.isSelected).length === 0)
newAcc.isSelected = true;
ctx.patchState({
accounts: [...state.accounts, newAcc]
});
@ -41,19 +44,25 @@ export class AccountsState {
@Action(SelectAccount)
SelectAccount(ctx: StateContext<AccountsStateModel>, action: SelectAccount){
const state = ctx.getState();
const multiSelection = action.multiselection;
// const multiSelection = action.multiselection;
const selectedAccount = action.account;
const copyAccounts = [...state.accounts];
if(!multiSelection) {
copyAccounts
.filter(x => x.id !== selectedAccount.id)
.forEach(x => x.isSelected = false);
}
const acc = copyAccounts.find(x => x.id === selectedAccount.id);
acc.isSelected = !acc.isSelected;
// const copyAccounts = [...state.accounts];
// copyAccounts
// .filter(x => x.id !== selectedAccount.id)
// .forEach(x => x.isSelected = false);
const oldSelectedAccount = state.accounts.find(x => x.isSelected);
if(selectedAccount.id === oldSelectedAccount.id) return;
const acc = state.accounts.find(x => x.id === selectedAccount.id);
acc.isSelected = true;
oldSelectedAccount.isSelected = false;
ctx.patchState({
accounts: copyAccounts
accounts: [...state.accounts]
});
}
@ -61,6 +70,10 @@ export class AccountsState {
RemoveAccount(ctx: StateContext<AccountsStateModel>, action: RemoveAccount){
const state = ctx.getState();
const filteredAccounts = state.accounts.filter(x => x.id !== action.accountId);
if(filteredAccounts.length === 1)
filteredAccounts[0].isSelected = true;
ctx.patchState({
accounts: filteredAccounts
});

BIN
src/assets/img/arrow_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/assets/img/arrow_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,7 +1,10 @@
.panel{
width: 100%;
// width: 100%;
width: calc(100%);
height: calc(100%);
padding: 10px 10px 0 7px;
font-size: $small-font-size;
white-space: normal;
&__title {
font-size: 13px;
text-transform: uppercase;

View File

@ -31,6 +31,7 @@ $favorite-color: #ffc16f;
$stream-selector-height: 30px;
$stream-column-separator: 7px;
$stream-column-width: 320px;
$floating-column-size: 330px;
$avatar-column-space: 70px;
//Bootstrap cuistomization