diff --git a/main.js b/main.js index 7857e9ee..b58c5eed 100644 --- a/main.js +++ b/main.js @@ -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'); diff --git a/src/app/app.component.html b/src/app/app.component.html index 2810e38f..b88ad534 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -5,11 +5,10 @@ -->
- - - - - + + + +
@@ -20,4 +19,4 @@ Welcome to {{ title }}! ---> +--> \ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 2690e025..ebb2e9dc 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d2d98276..12cf64ec 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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; + + 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(); - } - } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 601084fc..b6520117 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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] }) diff --git a/src/app/components/floating-column/add-new-account/add-new-account.component.ts b/src/app/components/floating-column/add-new-account/add-new-account.component.ts index 6ba64280..a9698cb4 100644 --- a/src/app/components/floating-column/add-new-account/add-new-account.component.ts +++ b/src/app/components/floating-column/add-new-account/add-new-account.component.ts @@ -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) => { diff --git a/src/app/components/floating-column/add-new-status/add-new-status.component.ts b/src/app/components/floating-column/add-new-status/add-new-status.component.ts index 6f471b24..69fa5720 100644 --- a/src/app/components/floating-column/add-new-status/add-new-status.component.ts +++ b/src/app/components/floating-column/add-new-status/add-new-status.component.ts @@ -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); }); } diff --git a/src/app/components/floating-column/floating-column.component.html b/src/app/components/floating-column/floating-column.component.html index 8ca112ed..fe9ef340 100644 --- a/src/app/components/floating-column/floating-column.component.html +++ b/src/app/components/floating-column/floating-column.component.html @@ -1,7 +1,8 @@
+ [browseHashtagData]="overlayHashtagToBrowse" + [browseThreadData]="overlayThreadToBrowse">
x @@ -12,6 +13,7 @@ + (browseHashtagEvent)="browseHashtag($event)" + (browseThreadEvent)="browseThread($event)">
\ No newline at end of file diff --git a/src/app/components/floating-column/floating-column.component.scss b/src/app/components/floating-column/floating-column.component.scss index dc5117a1..6f1e8a25 100644 --- a/src/app/components/floating-column/floating-column.component.scss +++ b/src/app/components/floating-column/floating-column.component.scss @@ -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 { // } diff --git a/src/app/components/floating-column/floating-column.component.ts b/src/app/components/floating-column/floating-column.component.ts index 8f5116eb..31f28386 100644 --- a/src/app/components/floating-column/floating-column.component.ts +++ b/src/app/components/floating-column/floating-column.component.ts @@ -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 { diff --git a/src/app/components/floating-column/manage-account/manage-account.component.ts b/src/app/components/floating-column/manage-account/manage-account.component.ts index dcbb2c92..295f1698 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.ts +++ b/src/app/components/floating-column/manage-account/manage-account.component.ts @@ -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; } diff --git a/src/app/components/floating-column/search/search.component.html b/src/app/components/floating-column/search/search.component.html index 1e2089e8..19247483 100644 --- a/src/app/components/floating-column/search/search.component.html +++ b/src/app/components/floating-column/search/search.component.html @@ -32,7 +32,8 @@
+ (browseHashtagEvent)="browseHashtag($event)" + (browseThreadEvent)="browseThread($event)">
\ No newline at end of file diff --git a/src/app/components/floating-column/search/search.component.scss b/src/app/components/floating-column/search/search.component.scss index ce4d5a7a..182c7275 100644 --- a/src/app/components/floating-column/search/search.component.scss +++ b/src/app/components/floating-column/search/search.component.scss @@ -56,7 +56,8 @@ } } - &__status { + &__status { + font-size: 15px; border-top: 1px solid $separator-color; &:last-of-type { border-bottom: 1px solid $separator-color; diff --git a/src/app/components/floating-column/search/search.component.ts b/src/app/components/floating-column/search/search.component.ts index dea93cbe..584aaed3 100644 --- a/src/app/components/floating-column/search/search.component.ts +++ b/src/app/components/floating-column/search/search.component.ts @@ -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(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); 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; }); } } diff --git a/src/app/components/left-side-bar/left-side-bar.component.html b/src/app/components/left-side-bar/left-side-bar.component.html index f9bbc44c..a5840902 100644 --- a/src/app/components/left-side-bar/left-side-bar.component.html +++ b/src/app/components/left-side-bar/left-side-bar.component.html @@ -1,8 +1,9 @@
- - + + + - + @@ -11,11 +12,11 @@
- + - + \ No newline at end of file diff --git a/src/app/components/left-side-bar/left-side-bar.component.scss b/src/app/components/left-side-bar/left-side-bar.component.scss index 052f750e..c1e9c22b 100644 --- a/src/app/components/left-side-bar/left-side-bar.component.scss +++ b/src/app/components/left-side-bar/left-side-bar.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/app/components/left-side-bar/left-side-bar.component.ts b/src/app/components/left-side-bar/left-side-bar.component.ts index 5e356da5..aeca183e 100644 --- a/src/app/components/left-side-bar/left-side-bar.component.ts +++ b/src/app/components/left-side-bar/left-side-bar.component.ts @@ -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; // 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); } diff --git a/src/app/components/notification-hub/notification-hub.component.html b/src/app/components/notification-hub/notification-hub.component.html new file mode 100644 index 00000000..1ddc6219 --- /dev/null +++ b/src/app/components/notification-hub/notification-hub.component.html @@ -0,0 +1,5 @@ +
+
+ {{ notification.message }} +
+
diff --git a/src/app/components/notification-hub/notification-hub.component.scss b/src/app/components/notification-hub/notification-hub.component.scss new file mode 100644 index 00000000..4d7cc64c --- /dev/null +++ b/src/app/components/notification-hub/notification-hub.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/src/app/components/notification-hub/notification-hub.component.spec.ts b/src/app/components/notification-hub/notification-hub.component.spec.ts new file mode 100644 index 00000000..8d5e16ff --- /dev/null +++ b/src/app/components/notification-hub/notification-hub.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotificationHubComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationHubComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/notification-hub/notification-hub.component.ts b/src/app/components/notification-hub/notification-hub.component.ts new file mode 100644 index 00000000..3b0ff926 --- /dev/null +++ b/src/app/components/notification-hub/notification-hub.component.ts @@ -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); + } +} diff --git a/src/app/components/stream/hashtag/hashtag.component.html b/src/app/components/stream/hashtag/hashtag.component.html index 7f13a754..dbf3f1c6 100644 --- a/src/app/components/stream/hashtag/hashtag.component.html +++ b/src/app/components/stream/hashtag/hashtag.component.html @@ -6,9 +6,10 @@ - diff --git a/src/app/components/stream/hashtag/hashtag.component.ts b/src/app/components/stream/hashtag/hashtag.component.ts index 7760d2a0..60bd7176 100644 --- a/src/app/components/stream/hashtag/hashtag.component.ts +++ b/src/app/components/stream/hashtag/hashtag.component.ts @@ -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(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); - @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 = new Subject(); + 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); } } diff --git a/src/app/components/stream/status/action-bar/action-bar.component.ts b/src/app/components/stream/status/action-bar/action-bar.component.ts index 84005dad..8dc3b769 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.ts +++ b/src/app/components/stream/status/action-bar/action-bar.component.ts @@ -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 = 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 = 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; } diff --git a/src/app/components/stream/status/reply-to-status/reply-to-status.component.ts b/src/app/components/stream/status/reply-to-status/reply-to-status.component.ts index bb30e61b..cacbde8a 100644 --- a/src/app/components/stream/status/reply-to-status/reply-to-status.component.ts +++ b/src/app/components/stream/status/reply-to-status/reply-to-status.component.ts @@ -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); }); } diff --git a/src/app/components/stream/status/status.component.ts b/src/app/components/stream/status/status.component.ts index 15a50e53..66e02e7b 100644 --- a/src/app/components/stream/status/status.component.ts +++ b/src/app/components/stream/status/status.component.ts @@ -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(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); 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); } } diff --git a/src/app/components/stream/stream-overlay/stream-overlay.component.html b/src/app/components/stream/stream-overlay/stream-overlay.component.html index 9d06ac34..8c03f613 100644 --- a/src/app/components/stream/stream-overlay/stream-overlay.component.html +++ b/src/app/components/stream/stream-overlay/stream-overlay.component.html @@ -6,15 +6,15 @@ NEXT - - - diff --git a/src/app/components/stream/stream-overlay/stream-overlay.component.ts b/src/app/components/stream/stream-overlay/stream-overlay.component.ts index 6dcdcd9d..cae5a329 100644 --- a/src/app/components/stream/stream-overlay/stream-overlay.component.ts +++ b/src/app/components/stream/stream-overlay/stream-overlay.component.ts @@ -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; diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.html b/src/app/components/stream/stream-statuses/stream-statuses.component.html index e9f6dec7..4df33f4f 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.html +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.html @@ -1,9 +1,13 @@
+
{{displayError}}
+
-
\ No newline at end of file diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.scss b/src/app/components/stream/stream-statuses/stream-statuses.component.scss index b7f225a0..8b3a5ac6 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.scss +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.scss @@ -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; diff --git a/src/app/components/stream/stream-statuses/stream-statuses.component.ts b/src/app/components/stream/stream-statuses/stream-statuses.component.ts index 9c486025..f95e67b6 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.ts +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.ts @@ -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(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); @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; + @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; diff --git a/src/app/components/stream/stream.component.html b/src/app/components/stream/stream.component.html index 8cb5b6f1..5e30ec4d 100644 --- a/src/app/components/stream/stream.component.html +++ b/src/app/components/stream/stream.component.html @@ -1,7 +1,9 @@
+ [browseAccountData]="overlayAccountToBrowse" + [browseHashtagData]="overlayHashtagToBrowse" + [browseThreadData]="overlayThreadToBrowse">
diff --git a/src/app/components/stream/stream.component.ts b/src/app/components/stream/stream.component.ts index dfdf7afe..8a252728 100644 --- a/src/app/components/stream/stream.component.ts +++ b/src/app/components/stream/stream.component.ts @@ -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 = new Subject(); @@ -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; } diff --git a/src/app/components/stream/thread/thread.component.ts b/src/app/components/stream/thread/thread.component.ts index 41077b18..c53487b0 100644 --- a/src/app/components/stream/thread/thread.component.ts +++ b/src/app/components/stream/thread/thread.component.ts @@ -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(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); @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 = 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) { + 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); } } diff --git a/src/app/components/stream/user-profile/user-profile.component.html b/src/app/components/stream/user-profile/user-profile.component.html index a819393d..c73b75e8 100644 --- a/src/app/components/stream/user-profile/user-profile.component.html +++ b/src/app/components/stream/user-profile/user-profile.component.html @@ -1,16 +1,16 @@ -
+
- -
+
+
+ + header +

{{account.display_name}}

+

@{{account.acct}}

+
+
+
(); @Output() browseHashtagEvent = new EventEmitter(); - @Output() browseThreadEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); @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 !== '

'; 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 { 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; - // }); } } diff --git a/src/app/components/tutorial/tutorial.component.html b/src/app/components/tutorial/tutorial.component.html new file mode 100644 index 00000000..6d5b6927 --- /dev/null +++ b/src/app/components/tutorial/tutorial.component.html @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/src/app/components/tutorial/tutorial.component.scss b/src/app/components/tutorial/tutorial.component.scss new file mode 100644 index 00000000..a5fcd38d --- /dev/null +++ b/src/app/components/tutorial/tutorial.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/app/components/tutorial/tutorial.component.spec.ts b/src/app/components/tutorial/tutorial.component.spec.ts new file mode 100644 index 00000000..705c6ee7 --- /dev/null +++ b/src/app/components/tutorial/tutorial.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TutorialComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TutorialComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/tutorial/tutorial.component.ts b/src/app/components/tutorial/tutorial.component.ts new file mode 100644 index 00000000..b8fa998a --- /dev/null +++ b/src/app/components/tutorial/tutorial.component.ts @@ -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; + @Select(state => state.registeredaccounts.accounts) accounts$: Observable; + + 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(); + } +} diff --git a/src/app/pages/register-new-account/register-new-account.component.ts b/src/app/pages/register-new-account/register-new-account.component.ts index 5163ae04..0ccf77a3 100644 --- a/src/app/pages/register-new-account/register-new-account.component.ts +++ b/src/app/pages/register-new-account/register-new-account.component.ts @@ -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); }); }); } diff --git a/src/app/services/notification.service.spec.ts b/src/app/services/notification.service.spec.ts new file mode 100644 index 00000000..f40b4207 --- /dev/null +++ b/src/app/services/notification.service.spec.ts @@ -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(); + })); +}); diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts new file mode 100644 index 00000000..63f2d753 --- /dev/null +++ b/src/app/services/notification.service.ts @@ -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(); + + 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()}`; + } +} diff --git a/src/app/services/tools.service.ts b/src/app/services/tools.service.ts index a2c24ca8..b07e8bde 100644 --- a/src/app/services/tools.service.ts +++ b/src/app/services/tools.service.ts @@ -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 { 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{ + const isProvider = originalStatus.provider.id === account.id; + + let statusPromise: Promise = 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 + ) { + } } diff --git a/src/app/states/accounts.state.ts b/src/app/states/accounts.state.ts index 1c5f6e0f..ae0be188 100644 --- a/src/app/states/accounts.state.ts +++ b/src/app/states/accounts.state.ts @@ -29,10 +29,13 @@ export interface AccountsStateModel { export class AccountsState { @Action(AddAccount) AddAccount(ctx: StateContext, 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, 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, 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 }); diff --git a/src/assets/img/arrow_1.png b/src/assets/img/arrow_1.png new file mode 100644 index 00000000..f74cd353 Binary files /dev/null and b/src/assets/img/arrow_1.png differ diff --git a/src/assets/img/arrow_2.png b/src/assets/img/arrow_2.png new file mode 100644 index 00000000..6946ad86 Binary files /dev/null and b/src/assets/img/arrow_2.png differ diff --git a/src/sass/_panel.scss b/src/sass/_panel.scss index 4178e73f..81ab3028 100644 --- a/src/sass/_panel.scss +++ b/src/sass/_panel.scss @@ -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; diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss index 554dd601..51db0290 100644 --- a/src/sass/_variables.scss +++ b/src/sass/_variables.scss @@ -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