diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1348ba4a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/main.js b/main-electron.js similarity index 100% rename from main.js rename to main-electron.js diff --git a/package.json b/package.json index 22d65311..2204c7ba 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "sengi", "version": "0.0.0", - "license": "MIT", - "main": "main.js", + "license": "AGPL-3.0-or-later", + "main": "main-electron.js", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 14ea1ce2..4e4143c3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -50,6 +50,12 @@ import { NotificationService } from "./services/notification.service"; import { MediaViewerComponent } from './components/media-viewer/media-viewer.component'; import { CreateStatusComponent } from './components/create-status/create-status.component'; import { MediaComponent } from './components/create-status/media/media.component'; +import { MyAccountComponent } from './components/floating-column/manage-account/my-account/my-account.component'; +import { FavoritesComponent } from './components/floating-column/manage-account/favorites/favorites.component'; +import { DirectMessagesComponent } from './components/floating-column/manage-account/direct-messages/direct-messages.component'; +import { MentionsComponent } from './components/floating-column/manage-account/mentions/mentions.component'; +import { NotificationsComponent } from './components/floating-column/manage-account/notifications/notifications.component'; +import { SettingsState } from './states/settings.state'; const routes: Routes = [ { path: "", redirectTo: "home", pathMatch: "full" }, @@ -89,7 +95,12 @@ const routes: Routes = [ NotificationHubComponent, MediaViewerComponent, CreateStatusComponent, - MediaComponent + MediaComponent, + MyAccountComponent, + FavoritesComponent, + DirectMessagesComponent, + MentionsComponent, + NotificationsComponent ], imports: [ FontAwesomeModule, @@ -102,7 +113,8 @@ const routes: Routes = [ NgxsModule.forRoot([ RegisteredAppsState, AccountsState, - StreamsState + StreamsState, + SettingsState ]), NgxsStoragePluginModule.forRoot() ], diff --git a/src/app/components/create-status/create-status.component.spec.ts b/src/app/components/create-status/create-status.component.spec.ts index 83364105..4d54ae7c 100644 --- a/src/app/components/create-status/create-status.component.spec.ts +++ b/src/app/components/create-status/create-status.component.spec.ts @@ -129,7 +129,6 @@ describe('CreateStatusComponent', () => { expect(result[1].length).toBeLessThanOrEqual(527); expect(result[0]).toContain('@Lorem@ipsum.com '); expect(result[1]).toContain('@Lorem@ipsum.com '); - console.warn(result); }); }); \ No newline at end of file diff --git a/src/app/components/floating-column/floating-column.component.html b/src/app/components/floating-column/floating-column.component.html index e7ed1bfa..418f0c4a 100644 --- a/src/app/components/floating-column/floating-column.component.html +++ b/src/app/components/floating-column/floating-column.component.html @@ -10,7 +10,11 @@ - + + direct-messages works! +

diff --git a/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.scss b/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.spec.ts b/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.spec.ts new file mode 100644 index 00000000..f64d0fe8 --- /dev/null +++ b/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DirectMessagesComponent } from './direct-messages.component'; + +xdescribe('DirectMessagesComponent', () => { + let component: DirectMessagesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DirectMessagesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DirectMessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.ts b/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.ts new file mode 100644 index 00000000..4b287c61 --- /dev/null +++ b/src/app/components/floating-column/manage-account/direct-messages/direct-messages.component.ts @@ -0,0 +1,119 @@ +import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; + +import { AccountWrapper } from '../../../../models/account.models'; +import { OpenThreadEvent } from '../../../../services/tools.service'; +import { StatusWrapper } from '../../../../models/common.model'; +import { NotificationService } from '../../../../services/notification.service'; +import { MastodonService } from '../../../../services/mastodon.service'; +import { StreamTypeEnum } from '../../../../states/streams.state'; +import { Status } from '../../../../services/models/mastodon.interfaces'; + +@Component({ + selector: 'app-direct-messages', + templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html', + styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './direct-messages.component.scss'] +}) +export class DirectMessagesComponent implements OnInit { + statuses: StatusWrapper[] = []; + displayError: string; + isLoading = true; + isThread = false; + hasContentWarnings = false; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + private maxReached = false; + private _account: AccountWrapper; + + @Input('account') + set account(acc: AccountWrapper) { + console.warn('account'); + this._account = acc; + this.getDirectMessages(); + } + get account(): AccountWrapper { + return this._account; + } + + @ViewChild('statusstream') public statustream: ElementRef; + + constructor( + private readonly notificationService: NotificationService, + private readonly mastodonService: MastodonService) { } + + ngOnInit() { + } + + private reset() { + this.isLoading = true; + this.statuses.length = 0; + this.maxReached = false; + } + + private getDirectMessages() { + this.reset(); + + this.mastodonService.getTimeline(this.account.info, StreamTypeEnum.directmessages) + .then((statuses: Status[]) => { + //this.maxId = statuses[statuses.length - 1].id; + for (const s of statuses) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + } + + onScroll() { + var element = this.statustream.nativeElement as HTMLElement; + const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000; + + if (atBottom) { + this.scrolledToBottom(); + } + } + + private scrolledToBottom() { + if (this.isLoading || this.maxReached) return; + + const maxId = this.statuses[this.statuses.length - 1].status.id; + this.isLoading = true; + this.mastodonService.getTimeline(this.account.info, StreamTypeEnum.directmessages, maxId) + .then((statuses: Status[]) => { + if (statuses.length === 0) { + this.maxReached = true; + return; + } + + for (const s of statuses) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + } + + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); + } +} diff --git a/src/app/components/floating-column/manage-account/favorites/favorites.component.html b/src/app/components/floating-column/manage-account/favorites/favorites.component.html new file mode 100644 index 00000000..5b593a28 --- /dev/null +++ b/src/app/components/floating-column/manage-account/favorites/favorites.component.html @@ -0,0 +1,3 @@ +

+ favorites works! +

diff --git a/src/app/components/floating-column/manage-account/favorites/favorites.component.scss b/src/app/components/floating-column/manage-account/favorites/favorites.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/floating-column/manage-account/favorites/favorites.component.spec.ts b/src/app/components/floating-column/manage-account/favorites/favorites.component.spec.ts new file mode 100644 index 00000000..1e356c74 --- /dev/null +++ b/src/app/components/floating-column/manage-account/favorites/favorites.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FavoritesComponent } from './favorites.component'; + +xdescribe('FavoritesComponent', () => { + let component: FavoritesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ FavoritesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(FavoritesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/manage-account/favorites/favorites.component.ts b/src/app/components/floating-column/manage-account/favorites/favorites.component.ts new file mode 100644 index 00000000..627f2f92 --- /dev/null +++ b/src/app/components/floating-column/manage-account/favorites/favorites.component.ts @@ -0,0 +1,123 @@ +import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core'; + +import { StatusWrapper } from '../../../../models/common.model'; +import { OpenThreadEvent } from '../../../../services/tools.service'; +import { AccountWrapper } from '../../../../models/account.models'; +import { MastodonService, FavoriteResult } from '../../../../services/mastodon.service'; +import { Status } from '../../../../services/models/mastodon.interfaces'; +import { NotificationService } from '../../../../services/notification.service'; +import { resetCompiledComponents } from '@angular/core/src/render3/jit/module'; + +@Component({ + selector: 'app-favorites', + templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html', + styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './favorites.component.scss'] +}) +export class FavoritesComponent implements OnInit { + statuses: StatusWrapper[] = []; + displayError: string; + isLoading = true; + isThread = false; + hasContentWarnings = false; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + private maxReached = false; + private maxId: string; + private _account: AccountWrapper; + + @Input('account') + set account(acc: AccountWrapper) { + this._account = acc; + this.getFavorites(); + } + get account(): AccountWrapper { + return this._account; + } + + @ViewChild('statusstream') public statustream: ElementRef; + + constructor( + private readonly notificationService: NotificationService, + private readonly mastodonService: MastodonService) { } + + ngOnInit() { + } + + private reset(){ + this.isLoading = true; + this.statuses.length = 0; + this.maxReached = false; + this.maxId = null; + } + + private getFavorites() { + this.reset(); + + this.mastodonService.getFavorites(this.account.info) + .then((result: FavoriteResult) => { + this.maxId = result.max_id; + for (const s of result.favorites) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + + } + + onScroll() { + var element = this.statustream.nativeElement as HTMLElement; + const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000; + + if (atBottom) { + this.scrolledToBottom(); + } + } + + + private scrolledToBottom() { + if (this.isLoading || this.maxReached) return; + + this.isLoading = true; + this.mastodonService.getFavorites(this.account.info, this.maxId) + .then((result: FavoriteResult) => { + const statuses = result.favorites; + if (statuses.length === 0 || !this.maxId) { + this.maxReached = true; + return; + } + + this.maxId = result.max_id; + for (const s of statuses) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + } + + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); + } +} diff --git a/src/app/components/floating-column/manage-account/manage-account.component.html b/src/app/components/floating-column/manage-account/manage-account.component.html index 08674412..37314bfb 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.html +++ b/src/app/components/floating-column/manage-account/manage-account.component.html @@ -1,28 +1,53 @@

Manage Account

- \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/manage-account.component.scss b/src/app/components/floating-column/manage-account/manage-account.component.scss index c31c4d31..d0bea638 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.scss +++ b/src/app/components/floating-column/manage-account/manage-account.component.scss @@ -1,67 +1,60 @@ @import "variables"; @import "panel"; -.account-editor { - // padding: 10px 10px 0 7px; - // font-size: $small-font-size; - // &__title { - // font-size: 13px; - // text-transform: uppercase; - // margin: 6px 0 12px 0; - // } - &__display-avatar { - text-align: center; - margin-bottom: 30px; - } - &__avatar { - // display: block; - width: 75px; - border-radius: 50px; - transform: translateX(15px); // margin: auto; - } +@import "commons"; +$account-header-height: 60px; +.panel { + padding-left: 0px; + padding-right: 0px; } .account { - &__label { - // text-decoration: underline; - font-size: $small-font-size; - margin-left: 5px; - color: $font-color-secondary; - } - &__margin-top { - margin-top: 25px; - } - &__link { - text-decoration: none; - display: block; // width: calc(100% - 20px); - width: 100%; // height: 30px; - padding: 5px 10px; // border: solid 1px black; - &:not(:last-child) { - margin-bottom: 5px; + &__header { + // padding-left: 10px; + padding-left: 5px; + padding-right: 10px; + padding-bottom: 5px; + height: $account-header-height; //border-top: 1px solid #222736; + border-bottom: 1px solid #222736; + &--button { + // outline: 1px greenyellow solid; + margin-top: 20px; + width: 35px; + height: 35px; + float: right; + margin-left: 5px; + font-size: 22px; + font-size: 20px; + color: $font-link-primary; + padding-left: 6px; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + transition: all .2s; + &:hover { + color: $font-link-primary-hover; + } + &--selected { + color: whitesmoke; + &:hover { + color: whitesmoke; + } + } + &--notification { + color: rgb(250, 152, 41); + &:hover { + color: rgb(255, 185, 106); + } + } } } - &__mid-link { - text-decoration: none; - display: block; // width: calc(100% - 20px); - width: 45%; // height: 30px; - padding: 5px 10px; // border: solid 1px black; - &:not(:last-child) { - margin-bottom: 5px; - } + &__avatar { + width: 50px; + border-radius: 3px; } - &__blue { - background-color: $color-primary; - color: #fff; - &:hover { - background-color: lighten($color-primary, 15); - } - } - - &__red { - $red-button-color: rgb(65, 3, 3); - background-color: $red-button-color; - color: #fff; - &:hover { - background-color: lighten($red-button-color, 15); - } + &__body { + overflow: auto; + height: calc(100% - #{$account-header-height} - 31px); + display: block; + font-size: $default-font-size; } } \ No newline at end of file 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 830d0af8..9bd9ff5b 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 @@ -1,48 +1,85 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../states/streams.state'; -import { Store } from '@ngxs/store'; -import { AccountsStateModel, AccountInfo, RemoveAccount } from '../../../states/accounts.state'; +import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons"; +import { faBell, faEnvelope, faUser, faStar } from "@fortawesome/free-regular-svg-icons"; +import { Subscription } from 'rxjs'; + import { AccountWrapper } from '../../../models/account.models'; -import { NavigationService } from '../../../services/navigation.service'; -import { NotificationService } from '../../../services/notification.service'; +import { UserNotificationService, UserNotification } from '../../../services/user-notification.service'; +import { OpenThreadEvent } from '../../../services/tools.service'; + @Component({ selector: 'app-manage-account', templateUrl: './manage-account.component.html', styleUrls: ['./manage-account.component.scss'] }) -export class ManageAccountComponent implements OnInit { - @Input() account: AccountWrapper; +export class ManageAccountComponent implements OnInit, OnDestroy { + faAt = faAt; + faBell = faBell; + faEnvelope = faEnvelope; + faUser = faUser; + faStar = faStar; + faUserPlus = faUserPlus; - availableStreams: StreamElement[] = []; + subPanel = 'account'; + hasNotifications = false; + hasMentions = false; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + @Input('account') + set account(acc: AccountWrapper) { + this._account = acc; + this.checkNotifications(); + } + get account(): AccountWrapper { + return this._account; + } + + private userNotificationServiceSub: Subscription; + private _account: AccountWrapper; constructor( - private readonly store: Store, - private readonly navigationService: NavigationService, - private notificationService: NotificationService) { } + private readonly userNotificationService: UserNotificationService) { } ngOnInit() { - const instance = this.account.info.instance; - this.availableStreams.length = 0; - this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, instance)); - this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, instance)); - this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, instance)); + } - addStream(stream: StreamElement): boolean { - if (stream) { - this.store.dispatch([new AddStream(stream)]).toPromise() - .then(() => { - this.notificationService.notify(`stream added`, false); - }); + ngOnDestroy(): void { + this.userNotificationServiceSub.unsubscribe(); + } + + private checkNotifications(){ + if(this.userNotificationServiceSub){ + this.userNotificationServiceSub.unsubscribe(); } + + this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => { + const userNotification = userNotifications.find(x => x.account.id === this.account.info.id); + if(userNotification){ + this.hasNotifications = userNotification.hasNewNotifications; + this.hasMentions = userNotification.hasNewMentions; + } + }); + } + + loadSubPanel(subpanel: string): boolean { + this.subPanel = subpanel; return false; } - removeAccount(): boolean { - const accountId = this.account.info.id; - this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]); - this.navigationService.closePanel(); - return false; + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); } } diff --git a/src/app/components/floating-column/manage-account/mentions/mentions.component.html b/src/app/components/floating-column/manage-account/mentions/mentions.component.html new file mode 100644 index 00000000..2989427b --- /dev/null +++ b/src/app/components/floating-column/manage-account/mentions/mentions.component.html @@ -0,0 +1,3 @@ +

+ mentions works! +

diff --git a/src/app/components/floating-column/manage-account/mentions/mentions.component.scss b/src/app/components/floating-column/manage-account/mentions/mentions.component.scss new file mode 100644 index 00000000..88c0dcf9 --- /dev/null +++ b/src/app/components/floating-column/manage-account/mentions/mentions.component.scss @@ -0,0 +1,7 @@ +@import "variables"; +@import "commons"; +@import "mixins"; + +.stream-toots { + background-color: $column-background; +} \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/mentions/mentions.component.spec.ts b/src/app/components/floating-column/manage-account/mentions/mentions.component.spec.ts new file mode 100644 index 00000000..2a25e786 --- /dev/null +++ b/src/app/components/floating-column/manage-account/mentions/mentions.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MentionsComponent } from './mentions.component'; + +xdescribe('MentionsComponent', () => { + let component: MentionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MentionsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MentionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/manage-account/mentions/mentions.component.ts b/src/app/components/floating-column/manage-account/mentions/mentions.component.ts new file mode 100644 index 00000000..222283f4 --- /dev/null +++ b/src/app/components/floating-column/manage-account/mentions/mentions.component.ts @@ -0,0 +1,137 @@ +import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { AccountWrapper } from '../../../../models/account.models'; +import { UserNotificationService, UserNotification } from '../../../../services/user-notification.service'; +import { StatusWrapper } from '../../../../models/common.model'; +import { Status, Notification } from '../../../../services/models/mastodon.interfaces'; +import { MastodonService } from '../../../../services/mastodon.service'; +import { NotificationService } from '../../../../services/notification.service'; +import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service'; + + +@Component({ + selector: 'app-mentions', + templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html', + styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './mentions.component.scss'] +}) +export class MentionsComponent implements OnInit, OnDestroy { + statuses: StatusWrapper[] = []; + displayError: string; + isLoading = false; + isThread = false; + hasContentWarnings = false; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + @Input('account') + set account(acc: AccountWrapper) { + console.warn('account'); + this._account = acc; + this.loadMentions(); + + const accountSettings = this.toolsService.getAccountSettings(acc.info); + console.warn(accountSettings); + } + get account(): AccountWrapper { + return this._account; + } + + @ViewChild('statusstream') public statustream: ElementRef; + + private maxReached = false; + private _account: AccountWrapper; + private userNotificationServiceSub: Subscription; + private lastId: string; + + constructor( + private readonly toolsService: ToolsService, + private readonly notificationService: NotificationService, + private readonly userNotificationService: UserNotificationService, + private readonly mastodonService: MastodonService) { + + } + + ngOnInit() { + } + + ngOnDestroy(): void { + if(this.userNotificationServiceSub){ + this.userNotificationServiceSub.unsubscribe(); + } + } + + private loadMentions(){ + if(this.userNotificationServiceSub){ + this.userNotificationServiceSub.unsubscribe(); + } + + this.statuses.length = 0; + this.userNotificationService.markMentionsAsRead(this.account.info); + + this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => { + this.statuses.length = 0; //TODO: don't reset, only add the new ones + const userNotification = userNotifications.find(x => x.account.id === this.account.info.id); + if(userNotification && userNotification.mentions){ + userNotification.mentions.forEach((mention: Status) => { + const statusWrapper = new StatusWrapper(mention, this.account.info); + this.statuses.push(statusWrapper); + }); + } + this.lastId = userNotification.lastId; + this.userNotificationService.markMentionsAsRead(this.account.info); + }); + } + + onScroll() { + var element = this.statustream.nativeElement as HTMLElement; + const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000; + + if (atBottom) { + this.scrolledToBottom(); + } + } + + private scrolledToBottom() { + if (this.isLoading || this.maxReached || this.statuses.length === 0) return; + + this.isLoading = true; + + this.mastodonService.getNotifications(this.account.info, ['follow', 'favourite', 'reblog'], this.lastId) + .then((result: Notification[]) => { + + const statuses = result.map(x => x.status); + if (statuses.length === 0) { + this.maxReached = true; + return; + } + + for (const s of statuses) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + + this.lastId = result[result.length - 1].id; + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + } + + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); + } +} diff --git a/src/app/components/floating-column/manage-account/my-account/my-account.component.html b/src/app/components/floating-column/manage-account/my-account/my-account.component.html new file mode 100644 index 00000000..82d03bde --- /dev/null +++ b/src/app/components/floating-column/manage-account/my-account/my-account.component.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/my-account/my-account.component.scss b/src/app/components/floating-column/manage-account/my-account/my-account.component.scss new file mode 100644 index 00000000..4d7ba144 --- /dev/null +++ b/src/app/components/floating-column/manage-account/my-account/my-account.component.scss @@ -0,0 +1,50 @@ +@import "variables"; +@import "commons"; + +.my-account { + transition: all .2s; + &__body { + overflow: auto; + height: calc(100%); + // width: calc(100%); + padding-left: 10px; + padding-right: 10px; + font-size: $small-font-size; + padding-bottom: 20px; + outline: 1px dotted greenyellow; + } + &__label { + // text-decoration: underline; + font-size: $small-font-size; + margin-top: 10px; + margin-left: 5px; + color: $font-color-secondary; + } + &__link { + text-decoration: none; + display: block; // width: calc(100% - 20px); + width: 100%; // height: 30px; + padding: 5px 10px; // border: solid 1px black; + &:not(:last-child) { + margin-bottom: 5px; + } + } + &__margin-top { + margin-top: 25px; + } + &__blue { + background-color: $color-primary; + color: #fff; + &:hover { + background-color: lighten($color-primary, 15); + } + } + &__red { + $red-button-color: rgb(65, 3, 3); + background-color: $red-button-color; + color: #fff; + &:hover { + background-color: lighten($red-button-color, 15); + } + } +} \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/my-account/my-account.component.spec.ts b/src/app/components/floating-column/manage-account/my-account/my-account.component.spec.ts new file mode 100644 index 00000000..bfa42339 --- /dev/null +++ b/src/app/components/floating-column/manage-account/my-account/my-account.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MyAccountComponent } from './my-account.component'; + +xdescribe('MyAccountComponent', () => { + let component: MyAccountComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MyAccountComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MyAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/manage-account/my-account/my-account.component.ts b/src/app/components/floating-column/manage-account/my-account/my-account.component.ts new file mode 100644 index 00000000..67a52474 --- /dev/null +++ b/src/app/components/floating-column/manage-account/my-account/my-account.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; + +import { NotificationService } from '../../../../services/notification.service'; +import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../../states/streams.state'; +import { AccountWrapper } from '../../../../models/account.models'; +import { RemoveAccount } from '../../../../states/accounts.state'; +import { NavigationService } from '../../../../services/navigation.service'; + +@Component({ + selector: 'app-my-account', + templateUrl: './my-account.component.html', + styleUrls: ['./my-account.component.scss'] +}) +export class MyAccountComponent implements OnInit { + + availableStreams: StreamElement[] = []; + + @Input() account: AccountWrapper; + + constructor( + private readonly store: Store, + private readonly navigationService: NavigationService, + private notificationService: NotificationService) { } + + ngOnInit() { + const instance = this.account.info.instance; + this.availableStreams.length = 0; + this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, instance)); + this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, instance)); + this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, instance)); + } + + addStream(stream: StreamElement): boolean { + if (stream) { + this.store.dispatch([new AddStream(stream)]).toPromise() + .then(() => { + this.notificationService.notify(`stream added`, false); + }); + } + return false; + } + + removeAccount(): boolean { + const accountId = this.account.info.id; + this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]); + this.navigationService.closePanel(); + return false; + } +} diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.html b/src/app/components/floating-column/manage-account/notifications/notifications.component.html new file mode 100644 index 00000000..69ce399f --- /dev/null +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.html @@ -0,0 +1,45 @@ + \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.scss b/src/app/components/floating-column/manage-account/notifications/notifications.component.scss new file mode 100644 index 00000000..177fdae3 --- /dev/null +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.scss @@ -0,0 +1,90 @@ +@import "variables"; +@import "commons"; +@import "mixins"; + +.stream { + height: calc(100%); + width: calc(100%); + overflow: auto; + background-color: $column-background; + + &__error { + padding: 20px 20px 0 20px; + color: rgb(255, 113, 113); + } + + &__notification { + position: relative; + + &--icon { + position: absolute; + top: 5px; + left: 43px; + text-align: center; + width: 20px; + // outline: 1px dotted greenyellow; + } + + &--label { + margin: 0 10px 0 $avatar-column-space; + padding-top: 5px; + } + + + &:not(:last-child) { + border: solid #06070b; + border-width: 0 0 1px 0; + } + } + + &__link { + color: $status-links-color; + } + + &__status { + display: block; + // opacity: 0.65; + } +} + +.followed { + color: $boost-color; +} + +.follow-account { + padding: 5px; + height: 60px; + width: calc(100%); + overflow: hidden; + display: block; + position: relative; + text-decoration: none; + + &__avatar { + float: left; + margin: 0 0 0 10px; + width: 45px; + height: 45px; + border-radius: 2px; + } + + $acccount-info-left: 70px; + &__display-name { + position: absolute; + top: 7px; + left: $acccount-info-left; + color: whitesmoke; + } + + &:hover &__display-name { + text-decoration: underline; + } + + &__acct { + position: absolute; + top: 27px; + left: $acccount-info-left; + font-size: 13px; + color: $status-links-color; + } +} \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.spec.ts b/src/app/components/floating-column/manage-account/notifications/notifications.component.spec.ts new file mode 100644 index 00000000..9efda899 --- /dev/null +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsComponent } from './notifications.component'; + +xdescribe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ NotificationsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.ts b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts new file mode 100644 index 00000000..34051570 --- /dev/null +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts @@ -0,0 +1,159 @@ +import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { faStar, faUserPlus, faRetweet } from "@fortawesome/free-solid-svg-icons"; +import { faStar as faStar2 } from "@fortawesome/free-regular-svg-icons"; + +import { AccountWrapper } from '../../../../models/account.models'; +import { UserNotificationService, UserNotification } from '../../../../services/user-notification.service'; +import { StatusWrapper } from '../../../../models/common.model'; +import { Notification, Account } from '../../../../services/models/mastodon.interfaces'; +import { MastodonService } from '../../../../services/mastodon.service'; +import { NotificationService } from '../../../../services/notification.service'; +import { AccountInfo } from '../../../../states/accounts.state'; +import { OpenThreadEvent } from '../../../../services/tools.service'; + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.scss'] +}) +export class NotificationsComponent implements OnInit, OnDestroy { + faUserPlus = faUserPlus; + // faStar = faStar; + // faRetweet = faRetweet; + + notifications: NotificationWrapper[] = []; + isLoading = false; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + @Input('account') + set account(acc: AccountWrapper) { + this._account = acc; + this.loadNotifications(); + } + get account(): AccountWrapper { + return this._account; + } + + @ViewChild('statusstream') public statustream: ElementRef; + + private maxReached = false; + private _account: AccountWrapper; + private userNotificationServiceSub: Subscription; + private lastId: string; + + constructor( + private readonly notificationService: NotificationService, + private readonly userNotificationService: UserNotificationService, + private readonly mastodonService: MastodonService) { } + + ngOnInit() { + } + + ngOnDestroy(): void { + if(this.userNotificationServiceSub){ + this.userNotificationServiceSub.unsubscribe(); + } + } + + private loadNotifications(){ + if(this.userNotificationServiceSub){ + this.userNotificationServiceSub.unsubscribe(); + } + + this.notifications.length = 0; + this.userNotificationService.markNotificationAsRead(this.account.info); + + this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => { + this.notifications.length = 0; //TODO: don't reset, only add the new ones + const userNotification = userNotifications.find(x => x.account.id === this.account.info.id); + if(userNotification && userNotification.notifications){ + userNotification.notifications.forEach((notification: Notification) => { + const notificationWrapper = new NotificationWrapper(notification, this.account.info); + this.notifications.push(notificationWrapper); + }); + } + this.lastId = userNotification.lastId; + this.userNotificationService.markNotificationAsRead(this.account.info); + }); + } + + + onScroll() { + var element = this.statustream.nativeElement as HTMLElement; + const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000; + + if (atBottom) { + this.scrolledToBottom(); + } + } + + private scrolledToBottom() { + if (this.isLoading || this.maxReached || this.notifications.length === 0) return; + + this.isLoading = true; + + this.mastodonService.getNotifications(this.account.info, ['mention'], this.lastId) + .then((notifications: Notification[]) => { + if (notifications.length === 0) { + this.maxReached = true; + return; + } + + for (const s of notifications) { + const wrapper = new NotificationWrapper(s, this.account.info); + this.notifications.push(wrapper); + } + + this.lastId = notifications[notifications.length - 1].id; + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.isLoading = false; + }); + } + + openAccount(account: Account): boolean { + let accountName = account.acct; + if (!accountName.includes('@')) + accountName += `@${account.url.replace('https://', '').split('/')[0]}`; + + this.browseAccountEvent.next(accountName); + return false; + } + + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); + } +} + +class NotificationWrapper { + constructor(notification: Notification, provider: AccountInfo) { + this.type = notification.type; + switch(this.type){ + case 'mention': + case 'reblog': + case 'favourite': + this.status= new StatusWrapper(notification.status, provider); + break; + } + this.account = notification.account; + } + + account: Account; + status: StatusWrapper; + type: 'mention' | 'reblog' | 'favourite' | 'follow'; +} \ No newline at end of file diff --git a/src/app/components/left-side-bar/account-icon/account-icon.component.html b/src/app/components/left-side-bar/account-icon/account-icon.component.html index b746150c..59d36ad6 100644 --- a/src/app/components/left-side-bar/account-icon/account-icon.component.html +++ b/src/app/components/left-side-bar/account-icon/account-icon.component.html @@ -1,4 +1,5 @@ diff --git a/src/app/components/left-side-bar/account-icon/account-icon.component.scss b/src/app/components/left-side-bar/account-icon/account-icon.component.scss index ac15d03f..0f6b57e1 100644 --- a/src/app/components/left-side-bar/account-icon/account-icon.component.scss +++ b/src/app/components/left-side-bar/account-icon/account-icon.component.scss @@ -1,45 +1,98 @@ .account-icon { display: inline-block; - width: 50px; - // padding-top: 4px; + width: 50px; // padding-top: 4px; // margin-left: 5px; margin: 0 0 5px 5px; - - - - &__avatar { - border-radius: 50%; border-radius: 2px; width: 40px; opacity: .3; transition: all .2s; - &:hover { filter: alpha(opacity=50); opacity: .5; } - &--selected { // border-radius: 20%; filter: alpha(opacity=100); opacity: 1; - &:hover { filter: alpha(opacity=100); opacity: 1; } - } } - - - // & a { - // margin-left: 4px; - // /*margin-top: 4px;*/ - // } - // & img { - // width: 40px; - // border-radius: 50%; - // } +} + +@keyframes flickerAnimation { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@-o-keyframes flickerAnimation { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@-moz-keyframes flickerAnimation { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@-webkit-keyframes flickerAnimation { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.hasActivity { + + -webkit-animation: flickerAnimation 2s infinite; + -moz-animation: flickerAnimation 2s infinite; + -o-animation: flickerAnimation 2s infinite; + animation: flickerAnimation 2s infinite; + + border-radius: 2px; + width: 40px; + height: 40px; + position: absolute; + border: 2px solid orange; + z-index: 20; + color: orange; + font-size: 10px; + font-style: italic; + padding: 23px 0 0 3px; + + background: rgba(0,0,0, .55); + + &:hover { + color: orange; + } } \ No newline at end of file diff --git a/src/app/components/left-side-bar/account-icon/account-icon.component.ts b/src/app/components/left-side-bar/account-icon/account-icon.component.ts index 5b3ed0cc..2346d91f 100644 --- a/src/app/components/left-side-bar/account-icon/account-icon.component.ts +++ b/src/app/components/left-side-bar/account-icon/account-icon.component.ts @@ -1,28 +1,30 @@ import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; + import { AccountWrapper } from '../../../models/account.models'; +import { AccountWithNotificationWrapper } from '../left-side-bar.component'; @Component({ - selector: 'app-account-icon', - templateUrl: './account-icon.component.html', - styleUrls: ['./account-icon.component.scss'] + selector: 'app-account-icon', + templateUrl: './account-icon.component.html', + styleUrls: ['./account-icon.component.scss'] }) export class AccountIconComponent implements OnInit { - @Input() account: AccountWrapper; - @Output() toogleAccountNotify = new EventEmitter(); - @Output() openMenuNotify = new EventEmitter(); + @Input() account: AccountWithNotificationWrapper; + @Output() toogleAccountNotify = new EventEmitter(); + @Output() openMenuNotify = new EventEmitter(); - constructor() { } + constructor() { } - ngOnInit() { - } + ngOnInit() { + } - toogleAccount(): boolean { - this.toogleAccountNotify.emit(this.account); - return false; - } + toogleAccount(): boolean { + this.toogleAccountNotify.emit(this.account); + return false; + } - openMenu(): boolean { - this.openMenuNotify.emit(this.account); - return false; - } + openMenu(): boolean { + this.openMenuNotify.emit(this.account); + return false; + } } 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 aeca183e..0b474703 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 @@ -10,6 +10,7 @@ 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"; +import { UserNotificationService, UserNotification } from '../../services/user-notification.service'; @Component({ selector: "app-left-side-bar", @@ -19,14 +20,15 @@ import { NotificationService } from "../../services/notification.service"; export class LeftSideBarComponent implements OnInit, OnDestroy { faCommentAlt = faCommentAlt; - accounts: AccountWrapper[] = []; + accounts: AccountWithNotificationWrapper[] = []; hasAccounts: boolean; private accounts$: Observable; - // private loadedAccounts: { [index: string]: AccountInfo } = {}; - private sub: Subscription; + private accountSub: Subscription; + private notificationSub: Subscription; constructor( + private readonly userNotificationServiceService: UserNotificationService, private readonly notificationService: NotificationService, private readonly navigationService: NavigationService, private readonly mastodonService: MastodonService, @@ -37,7 +39,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy { private currentLoading: number; ngOnInit() { - this.accounts$.subscribe((accounts: AccountInfo[]) => { + this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { if (accounts) { //Update and Add for (let acc of accounts) { @@ -45,8 +47,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy { if (previousAcc) { previousAcc.info.isSelected = acc.isSelected; } else { - const accWrapper = new AccountWrapper(); + const accWrapper = new AccountWithNotificationWrapper(); accWrapper.info = acc; + this.accounts.push(accWrapper); this.mastodonService.retrieveAccountDetails(acc) @@ -61,17 +64,31 @@ export class LeftSideBarComponent implements OnInit, OnDestroy { //Delete const deletedAccounts = this.accounts.filter(x => accounts.findIndex(y => y.id === x.info.id) === -1); - for(let delAcc of deletedAccounts){ + for (let delAcc of deletedAccounts) { this.accounts = this.accounts.filter(x => x.info.id !== delAcc.info.id); } this.hasAccounts = this.accounts.length > 0; } }); + + this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => { + + notifications.forEach((notification: UserNotification) => { + const acc = this.accounts.find(x => x.info.id === notification.account.id); + if(acc){ + acc.hasActivityNotifications = notification.hasNewMentions || notification.hasNewNotifications; + } + }); + + console.warn('new notifications'); + console.warn(notifications); + }); } ngOnDestroy(): void { - this.sub.unsubscribe(); + this.accountSub.unsubscribe(); + this.notificationSub.unsubscribe(); } onToogleAccountNotify(acc: AccountWrapper) { @@ -102,3 +119,14 @@ export class LeftSideBarComponent implements OnInit, OnDestroy { return false; } } + +export class AccountWithNotificationWrapper extends AccountWrapper { + // constructor(accountWrapper: AccountWrapper) { + // super(); + + // this.avatar = accountWrapper.avatar; + // this.info = accountWrapper.info; + // } + + hasActivityNotifications: boolean; +} \ No newline at end of file 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 96db14bd..db21919f 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 @@ -119,10 +119,12 @@ export class ActionBarComponent implements OnInit, OnDestroy { const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper); usableStatus .then((status: Status) => { - if (this.isBoosted) { + if (this.isBoosted && status.reblogged) { return this.mastodonService.unreblog(account, status); - } else { + } else if(!this.isBoosted && !status.reblogged){ return this.mastodonService.reblog(account, status); + } else { + return Promise.resolve(status); } }) .then((boostedStatus: Status) => { @@ -144,10 +146,12 @@ export class ActionBarComponent implements OnInit, OnDestroy { const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper); usableStatus .then((status: Status) => { - if (this.isFavorited) { + if (this.isFavorited && status.favourited) { return this.mastodonService.unfavorite(account, status); - } else { + } else if(!this.isFavorited && !status.favourited) { return this.mastodonService.favorite(account, status); + } else { + return Promise.resolve(status); } }) .then((favoritedStatus: Status) => { diff --git a/src/app/components/stream/status/attachements/attachements.component.html b/src/app/components/stream/status/attachements/attachements.component.html index c7927631..c7e0c32b 100644 --- a/src/app/components/stream/status/attachements/attachements.component.html +++ b/src/app/components/stream/status/attachements/attachements.component.html @@ -15,7 +15,7 @@ + title="{{ attachments[0].description }}" (click)="attachmentSelected(0)"> + title="{{ attachments[0].description }}" (click)="attachmentSelected(0)"> {{ status.account.display_name }} boosted
+
+
+ +
+
+ {{ notificationAccount.display_name }} favorited your status +
+
+
+
+ +
+
+ {{ notificationAccount.display_name }} boosted your status +
+
- - - - - - {{displayedStatus.account.acct}} - - - \ No newline at end of file diff --git a/src/app/components/stream/status/status.component.scss b/src/app/components/stream/status/status.component.scss index dec8eb16..29293aee 100644 --- a/src/app/components/stream/status/status.component.scss +++ b/src/app/components/stream/status/status.component.scss @@ -153,4 +153,53 @@ .attachments { display: block; // width: calc(100% - 80px); margin: 10px 10px 0 $avatar-column-space; -} \ No newline at end of file +} + +.notification { + position: relative; + + &--icon { + position: absolute; + top: 5px; + left: 43px; + text-align: center; + width: 20px; + // outline: 1px dotted greenyellow; + } + + &--label { + margin: 0 10px 0 $avatar-column-space; + padding-top: 5px; + } + + &--link { + color: $status-links-color; + } + + &--status:not(.reply-section) { + opacity: 0.65; + } + + &--avatar { + position: absolute; + top: 35px; + left: 30px; + width: 30px; + height: 30px; + border-radius: 2px; + z-index: 10; + } + + // &:not(:last-child) { + // border: solid #06070b; + // border-width: 0 0 1px 0; + // } +} + +.boost { + color: $boost-color; +} + +.favorite { + color: $favorite-color; +} diff --git a/src/app/components/stream/status/status.component.ts b/src/app/components/stream/status/status.component.ts index f8299c8e..361ff5d9 100644 --- a/src/app/components/stream/status/status.component.ts +++ b/src/app/components/stream/status/status.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from "@angular/core"; +import { faStar, faRetweet } from "@fortawesome/free-solid-svg-icons"; + import { Status, Account } from "../../../services/models/mastodon.interfaces"; import { OpenThreadEvent } from "../../../services/tools.service"; import { ActionBarComponent } from "./action-bar/action-bar.component"; @@ -10,6 +12,9 @@ import { StatusWrapper } from '../../../models/common.model'; styleUrls: ["./status.component.scss"] }) export class StatusComponent implements OnInit { + faStar = faStar; + faRetweet = faRetweet; + displayedStatus: Status; reblog: boolean; hasAttachments: boolean; @@ -27,6 +32,9 @@ export class StatusComponent implements OnInit { @Input() isThreadDisplay: boolean; + @Input() notificationType: 'mention' | 'reblog' | 'favourite'; + @Input() notificationAccount: Account; + private _statusWrapper: StatusWrapper; status: Status; @Input('statusWrapper') @@ -92,7 +100,7 @@ export class StatusComponent implements OnInit { } } - if(this.isThreadDisplay) return; + if (this.isThreadDisplay) return; if (status.in_reply_to_account_id && status.in_reply_to_account_id === status.account.id) { this.isThread = true; 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 a57eb471..16a7edb0 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.html +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.html @@ -1,4 +1,8 @@
+
+ +
{{displayError}}
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 8b3a5ac6..2b625965 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.scss +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.scss @@ -1,19 +1,49 @@ @import "variables"; @import "commons"; - +@import "mixins"; .stream-toots { height: calc(100%); 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; } + &__remove-cw { + padding: 5px; + // border: solid #06070b; + // border-width: 0 0 1px 0; + height: 45px; + // width: calc(100%); + // position: relative; + + &--button { + @include clearButton; + + // position: absolute; + // width: calc(80%); + // margin-left: 40%; + // transform: translateX(-40%); + + width: calc(100%); + padding: 5px 0; + + z-index: 10; + text-align: center; + border: 3px $status-secondary-color double; + transition: all .2s; + background-color: $color-secondary; + + &:hover{ + $hover-color: $status-secondary-color; + background-color: $hover-color; + color: white; + border: 3px $hover-color double; + } + } + } } \ No newline at end of file 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 0667b2f9..ad11eec8 100644 --- a/src/app/components/stream/stream-statuses/stream-statuses.component.ts +++ b/src/app/components/stream/stream-statuses/stream-statuses.component.ts @@ -21,6 +21,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy { isLoading = true; isThread = false; displayError: string; + hasContentWarnings = false; private _streamElement: StreamElement; private account: AccountInfo; diff --git a/src/app/components/stream/thread/thread.component.ts b/src/app/components/stream/thread/thread.component.ts index 911b97ee..0e7a794f 100644 --- a/src/app/components/stream/thread/thread.component.ts +++ b/src/app/components/stream/thread/thread.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter, ViewChildren, QueryList } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { MastodonService } from '../../../services/mastodon.service'; @@ -7,6 +7,7 @@ import { Results, Context, Status } from '../../../services/models/mastodon.inte import { NotificationService } from '../../../services/notification.service'; import { AccountInfo } from '../../../states/accounts.state'; import { StatusWrapper } from '../../../models/common.model'; +import { StatusComponent } from '../status/status.component'; @Component({ selector: 'app-thread', @@ -18,6 +19,7 @@ export class ThreadComponent implements OnInit { displayError: string; isLoading = true; isThread = true; + hasContentWarnings = false; private lastThreadEvent: OpenThreadEvent; @@ -33,6 +35,8 @@ export class ThreadComponent implements OnInit { } } + @ViewChildren(StatusComponent) statusChildren: QueryList; + constructor( private readonly notificationService: NotificationService, private readonly toolsService: ToolsService, @@ -86,6 +90,8 @@ export class ThreadComponent implements OnInit { const wrapper = new StatusWrapper(s, currentAccount); this.statuses.push(wrapper); } + + this.hasContentWarnings = this.statuses.filter(x => x.status.sensitive || x.status.spoiler_text).length > 1; }); }) @@ -119,4 +125,12 @@ export class ThreadComponent implements OnInit { browseThread(openThreadEvent: OpenThreadEvent): void { this.browseThreadEvent.next(openThreadEvent); } + + removeCw(){ + const statuses = this.statusChildren.toArray(); + statuses.forEach(x => { + x.removeContentWarning(); + }); + this.hasContentWarnings = false; + } } diff --git a/src/app/components/stream/user-profile/user-profile.component.ts b/src/app/components/stream/user-profile/user-profile.component.ts index 0793e7e2..ee1dc4fa 100644 --- a/src/app/components/stream/user-profile/user-profile.component.ts +++ b/src/app/components/stream/user-profile/user-profile.component.ts @@ -90,9 +90,6 @@ export class UserProfileComponent implements OnInit { return this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName) .then((account: Account) => { - - console.warn(account); - this.isLoading = false; this.statusLoading = true; diff --git a/src/app/services/mastodon.service.ts b/src/app/services/mastodon.service.ts index 195b1c38..68048f6b 100644 --- a/src/app/services/mastodon.service.ts +++ b/src/app/services/mastodon.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core'; -import { HttpHeaders, HttpClient } from '@angular/common/http'; +import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http'; import { ApiRoutes } from './models/api.settings'; -import { Account, Status, Results, Context, Relationship, Instance, Attachment } from "./models/mastodon.interfaces"; +import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification } from "./models/mastodon.interfaces"; import { AccountInfo } from '../states/accounts.state'; import { StreamTypeEnum } from '../states/streams.state'; @Injectable() -export class MastodonService { +export class MastodonService { private apiRoutes = new ApiRoutes(); constructor(private readonly httpClient: HttpClient) { } @@ -134,6 +134,27 @@ export class MastodonService { return this.httpClient.get(route, { headers: headers }).toPromise(); } + getFavorites(account: AccountInfo, maxId: string = null): Promise { //, minId: string = null + let route = `https://${account.instance}${this.apiRoutes.getFavourites}`; //?limit=${limit} + + if (maxId) route += `?max_id=${maxId}`; + //if (minId) route += `&min_id=${minId}`; + + const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); + return this.httpClient.get(route, { headers: headers, observe: "response" }).toPromise() + .then((res: HttpResponse) => { + const link = res.headers.get('Link'); + let lastId = null; + if(link){ + const maxId = link.split('max_id=')[1]; + if(maxId){ + lastId = maxId.split('>;')[0]; + } + } + return new FavoriteResult(lastId, res.body); + }); + } + searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false): Promise { const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`; const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); @@ -152,20 +173,18 @@ export class MastodonService { return this.httpClient.post(route, null, { headers: headers }).toPromise() } - favorite(account: AccountInfo, status: Status): any { + favorite(account: AccountInfo, status: Status): Promise { const route = `https://${account.instance}${this.apiRoutes.favouritingStatus}`.replace('{0}', status.id); const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); return this.httpClient.post(route, null, { headers: headers }).toPromise() } - unfavorite(account: AccountInfo, status: Status): any { + unfavorite(account: AccountInfo, status: Status): Promise { const route = `https://${account.instance}${this.apiRoutes.unfavouritingStatus}`.replace('{0}', status.id); const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); return this.httpClient.post(route, null, { headers: headers }).toPromise() } - - getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise { let params = `?${this.formatArray(accountsToRetrieve.map(x => x.id.toString()), 'id')}`; @@ -202,7 +221,7 @@ export class MastodonService { } //TODO: add focus support - updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise { + updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise { let input = new FormData(); input.append('description', description); const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`; @@ -210,10 +229,30 @@ export class MastodonService { return this.httpClient.put(route, input, { headers: headers }).toPromise(); } + getNotifications(account: AccountInfo, excludeTypes: string[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise { + let route = `https://${account.instance}${this.apiRoutes.getNotifications}?limit=${limit}`; + + if(maxId){ + route += `&max_id=${maxId}`; + } + + if(sinceId){ + route += `&since_id=${sinceId}`; + } + + if(excludeTypes && excludeTypes.length > 0) { + const excludeTypeArray = this.formatArray(excludeTypes, 'exclude_types'); + route += `&${excludeTypeArray}`; + } + + const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); + return this.httpClient.get(route, { headers: headers }).toPromise(); + } + private formatArray(data: string[], paramName: string): string { let result = ''; data.forEach(x => { - if (result.includes('paramName')) result += '&'; + if (result.includes(paramName)) result += '&'; result += `${paramName}[]=${x}`; }); return result; @@ -235,4 +274,10 @@ class StatusData { sensitive: boolean; spoiler_text: string; visibility: string; +} + +export class FavoriteResult { + constructor( + public max_id: string, + public favorites: Status[]) {} } \ No newline at end of file diff --git a/src/app/services/models/mastodon.interfaces.ts b/src/app/services/models/mastodon.interfaces.ts index fdcbcfec..bcbf001e 100644 --- a/src/app/services/models/mastodon.interfaces.ts +++ b/src/app/services/models/mastodon.interfaces.ts @@ -11,7 +11,12 @@ export interface TokenData { access_token: string; token_type: string; scope: string; - created_at: string; + created_at: number; + + //TODO: Pleroma support this + me: string; + expires_in: number; + refresh_token: string; } export interface Account { diff --git a/src/app/services/tools.service.ts b/src/app/services/tools.service.ts index 1cf87b00..0e444c68 100644 --- a/src/app/services/tools.service.ts +++ b/src/app/services/tools.service.ts @@ -5,12 +5,13 @@ import { AccountInfo } from '../states/accounts.state'; import { MastodonService } from './mastodon.service'; import { Account, Results, Status } from "./models/mastodon.interfaces"; import { StatusWrapper } from '../models/common.model'; +import { AccountSettings, SaveAccountSettings } from '../states/settings.state'; @Injectable({ providedIn: 'root' }) export class ToolsService { - + constructor( private readonly mastodonService: MastodonService, private readonly store: Store) { } @@ -20,6 +21,23 @@ export class ToolsService { var regAccounts = this.store.snapshot().registeredaccounts.accounts; return regAccounts.filter(x => x.isSelected); } + + getAccountSettings(account: AccountInfo): AccountSettings { + var accountsSettings = this.store.snapshot().globalsettings.settings.accountSettings; + let accountSettings = accountsSettings.find(x => x.accountId === account.id); + if(!accountSettings){ + accountSettings = new AccountSettings(); + accountSettings.accountId = account.id; + this.saveAccountSettings(accountSettings); + } + return accountSettings; + } + + saveAccountSettings(accountSettings: AccountSettings){ + this.store.dispatch([ + new SaveAccountSettings(accountSettings) + ]) + } findAccount(account: AccountInfo, accountName: string): Promise { return this.mastodonService.search(account, accountName, true) @@ -42,7 +60,7 @@ export class ToolsService { if (!isProvider) { statusPromise = statusPromise.then((foreignStatus: Status) => { const statusUrl = foreignStatus.url; - return this.mastodonService.search(account, statusUrl) + return this.mastodonService.search(account, statusUrl, true) .then((results: Results) => { return results.statuses[0]; }); @@ -51,6 +69,7 @@ export class ToolsService { return statusPromise; } + } export class OpenThreadEvent { diff --git a/src/app/services/user-notification.service.spec.ts b/src/app/services/user-notification.service.spec.ts new file mode 100644 index 00000000..25257910 --- /dev/null +++ b/src/app/services/user-notification.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserNotificationService } from './user-notification.service'; + +xdescribe('UserNotificationServiceService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: UserNotificationService = TestBed.get(UserNotificationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/user-notification.service.ts b/src/app/services/user-notification.service.ts new file mode 100644 index 00000000..c1b6a5fd --- /dev/null +++ b/src/app/services/user-notification.service.ts @@ -0,0 +1,202 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Subject, Observable, Subscription } from 'rxjs'; +import { Store } from '@ngxs/store'; + +import { Status, Notification } from './models/mastodon.interfaces'; +import { MastodonService } from './mastodon.service'; +import { AccountInfo } from '../states/accounts.state'; +import { NotificationService } from './notification.service'; +import { ToolsService } from './tools.service'; + +@Injectable({ + providedIn: 'root' +}) +export class UserNotificationService { + userNotifications = new BehaviorSubject([]); + + private sinceIds: { [id: string]: string } = {}; + + constructor( + private readonly toolsService: ToolsService, + private readonly notificationService: NotificationService, + private readonly mastodonService: MastodonService, + private readonly store: Store) { + + this.fetchNotifications(); + } + + private fetchNotifications() { + let accounts = this.store.snapshot().registeredaccounts.accounts; + let promises: Promise[] = []; + + accounts.forEach((account: AccountInfo) => { + let sinceId = null; + if (this.sinceIds[account.id]) { + sinceId = this.sinceIds[account.id]; + } + + let getNotificationPromise = this.mastodonService.getNotifications(account, null, null, sinceId, 30) + .then((notifications: Notification[]) => { + this.processNotifications(account, notifications); + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }); + promises.push(getNotificationPromise); + }); + + Promise.all(promises) + .then(() => { + setTimeout(() => { + this.fetchNotifications(); + }, 15 * 1000); + }); + } + + private processNotifications(account: AccountInfo, notifications: Notification[]) { + if (notifications.length === 0) { + return; + } + + let currentNotifications = this.userNotifications.value; + let currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id); + + const sinceId = notifications[0].id; + this.sinceIds[account.id] = sinceId; + + if (currentAccountNotifications) { + currentAccountNotifications.allNotifications = [...notifications, ...currentAccountNotifications.allNotifications]; + + currentAccountNotifications = this.analyseNotifications(account, currentAccountNotifications); + + if (currentAccountNotifications.hasNewMentions || currentAccountNotifications.hasNewNotifications) { + currentNotifications = currentNotifications.filter(x => x.account.id !== account.id); + currentNotifications.push(currentAccountNotifications); + this.userNotifications.next(currentNotifications); + } + } else { + let newNotifications = new UserNotification(); + newNotifications.account = account; + newNotifications.allNotifications = notifications; + + newNotifications = this.analyseNotifications(account, newNotifications); + + currentNotifications.push(newNotifications); + this.userNotifications.next(currentNotifications); + } + } + + private analyseNotifications(account: AccountInfo, userNotification: UserNotification): UserNotification { + if (userNotification.allNotifications.length > 30) { + userNotification.allNotifications.length = 30; + } + userNotification.lastId = userNotification.allNotifications[userNotification.allNotifications.length - 1].id; + + const newNotifications = userNotification.allNotifications.filter(x => x.type !== 'mention'); + const newMentions = userNotification.allNotifications.filter(x => x.type === 'mention').map(x => x.status); + + const currentNotifications = userNotification.notifications; + const currentMentions = userNotification.mentions; + + userNotification.notifications = [...newNotifications, ...currentNotifications]; + userNotification.mentions = [...newMentions, ...currentMentions]; + + const accountSettings = this.toolsService.getAccountSettings(account); + + if(accountSettings.lastMentionReadId && userNotification.mentions[0] && accountSettings.lastMentionReadId !== userNotification.mentions[0].id){ + userNotification.hasNewMentions = true; + } else { + userNotification.hasNewMentions = false; + } + + if(accountSettings.lastNotificationReadId && userNotification.notifications[0] && accountSettings.lastNotificationReadId !== userNotification.notifications[0].id){ + userNotification.hasNewNotifications = true; + } else { + userNotification.hasNewNotifications = false; + } + + if((!accountSettings.lastMentionReadId && userNotification.mentions[0]) + || (!accountSettings.lastNotificationReadId && userNotification.notifications[0])){ + accountSettings.lastMentionReadId = userNotification.mentions[0].id; + accountSettings.lastNotificationReadId = userNotification.notifications[0].id; + this.toolsService.saveAccountSettings(accountSettings); + } + + // if (!currentNotifications) { + // userNotification.notifications = newNotifications; + + // } else if (currentNotifications.length === 0) { + // if (newNotifications.length > 0) { + // userNotification.hasNewNotifications = true; + // } + // userNotification.notifications = newNotifications; + + // } else if (newNotifications.length > 0) { + // userNotification.hasNewNotifications = currentNotifications[0].id !== newNotifications[0].id; + // userNotification.notifications = [...newNotifications, ...currentNotifications]; + // } + + // if (!currentNotifications) { + // userNotification.mentions = newMentions; + + // } else if (currentMentions.length === 0) { + // if (newMentions.length > 0) { + // userNotification.hasNewMentions = true; + // } + // userNotification.mentions = newMentions; + + // } else if (newMentions.length > 0) { + // userNotification.hasNewMentions = currentMentions[0].id !== newMentions[0].id; + // userNotification.mentions = [...newMentions, ...currentMentions]; + // } + + return userNotification; + } + + markMentionsAsRead(account: AccountInfo) { + let currentNotifications = this.userNotifications.value; + const currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id); + + const lastMention = currentAccountNotifications.mentions[0]; + if(lastMention){ + // const lastNotification = currentAccountNotifications.allNotifications.find(x => x.status && x.status.id === lastMention.id); + const settings = this.toolsService.getAccountSettings(account); + settings.lastMentionReadId = lastMention.id; + this.toolsService.saveAccountSettings(settings); + } + + if (currentAccountNotifications.hasNewMentions === true) { + currentAccountNotifications.hasNewMentions = false; + this.userNotifications.next(currentNotifications); + } + } + + markNotificationAsRead(account: AccountInfo) { + let currentNotifications = this.userNotifications.value; + const currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id); + + const lastNotification = currentAccountNotifications.notifications[0]; + if(lastNotification){ + const settings = this.toolsService.getAccountSettings(account); + settings.lastNotificationReadId = lastNotification.id; + this.toolsService.saveAccountSettings(settings); + } + + if (currentAccountNotifications.hasNewNotifications === true) { + currentAccountNotifications.hasNewNotifications = false; + this.userNotifications.next(currentNotifications); + } + } +} + +export class UserNotification { + account: AccountInfo; + allNotifications: Notification[] = []; + + hasNewNotifications: boolean; + hasNewMentions: boolean; + + notifications: Notification[] = []; + mentions: Status[] = []; + lastId: string; +} diff --git a/src/app/states/settings.state.ts b/src/app/states/settings.state.ts new file mode 100644 index 00000000..e86b3eb3 --- /dev/null +++ b/src/app/states/settings.state.ts @@ -0,0 +1,88 @@ +import { State, Action, StateContext, Selector, createSelector } from '@ngxs/store'; + +export class RemoveAccountSettings { + static readonly type = '[Settings] Remove AccountSettings'; + constructor(public accountId: string) {} +} + +export class SaveAccountSettings { + static readonly type = '[Settings] Save AccountSettings'; + constructor(public accountSettings: AccountSettings) {} +} + +export class SaveSettings { + static readonly type = '[Settings] Save Settings'; + constructor(public settings: GlobalSettings) {} +} + +export class AccountSettings { + accountId: string; + displayMention: boolean = true; + displayNotifications: boolean = true; + lastMentionReadId: string; + lastNotificationReadId: string; +} + +export class GlobalSettings { + disableAllNotifications = false; + accountSettings: AccountSettings[] = []; +} + +export interface SettingsStateModel { + settings: GlobalSettings; +} + +@State({ + name: 'globalsettings', + defaults: { + settings: new GlobalSettings() + } +}) +export class SettingsState { + + accountSettings(accountId: string){ + return createSelector([SettingsState], (state: GlobalSettings) => { + return state.accountSettings.find(x => x.accountId === accountId); + }); + } + + + @Action(RemoveAccountSettings) + RemoveAccountSettings(ctx: StateContext, action: RemoveAccountSettings){ + const state = ctx.getState(); + const newSettings = new GlobalSettings(); + + newSettings.disableAllNotifications = state.settings.disableAllNotifications; + newSettings.accountSettings = [...state.settings.accountSettings.filter(x => x.accountId !== action.accountId)]; + + ctx.patchState({ + settings: newSettings + }); + } + + @Action(SaveAccountSettings) + SaveAccountSettings(ctx: StateContext, action: SaveAccountSettings){ + const state = ctx.getState(); + const newSettings = new GlobalSettings(); + + newSettings.disableAllNotifications = state.settings.disableAllNotifications; + newSettings.accountSettings = [...state.settings.accountSettings.filter(x => x.accountId !== action.accountSettings.accountId), action.accountSettings]; + + ctx.patchState({ + settings: newSettings + }); + } + + @Action(SaveSettings) + SaveSettings(ctx: StateContext, action: SaveSettings){ + const state = ctx.getState(); + const newSettings = new GlobalSettings(); + + newSettings.disableAllNotifications = action.settings.disableAllNotifications; + newSettings.accountSettings = [...state.settings.accountSettings]; + + ctx.patchState({ + settings: newSettings + }); + } +} \ No newline at end of file diff --git a/src/sass/_panel.scss b/src/sass/_panel.scss index 02d967f0..cc7f5a29 100644 --- a/src/sass/_panel.scss +++ b/src/sass/_panel.scss @@ -3,9 +3,8 @@ width: calc(100%); height: calc(100%); padding: 10px 10px 0 7px; - font-size: $small-font-size; + font-size: $small-font-size; //FIXME: remove this white-space: normal; - // overflow: auto; &__title { font-size: 13px; text-transform: uppercase; diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss index a8e037d2..0c2bfcdb 100644 --- a/src/sass/_variables.scss +++ b/src/sass/_variables.scss @@ -46,4 +46,8 @@ $button-size: 30px; $button-color: darken(white, 30); $button-color-hover: white; $button-background-color: $color-primary; -$button-background-color-hover: lighten($color-primary, 20); \ No newline at end of file +$button-background-color-hover: lighten($color-primary, 20); + + + +$column-background: #0f111a; \ No newline at end of file