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 @@
+
+
add column:
+
+ {{ stream.name }}
+
+
remove account from sengi:
+
+ Delete
+
+
\ 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 @@
-
+ new
+
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
+
+
-
-
-
-
-
- {{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