Merge pull request #69 from NicolasConstant/feature_add-notifications
Feature add notifications
This commit is contained in:
commit
068c3d4163
|
@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "sengi",
|
"name": "sengi",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "main.js",
|
"main": "main-electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
|
|
@ -50,6 +50,12 @@ import { NotificationService } from "./services/notification.service";
|
||||||
import { MediaViewerComponent } from './components/media-viewer/media-viewer.component';
|
import { MediaViewerComponent } from './components/media-viewer/media-viewer.component';
|
||||||
import { CreateStatusComponent } from './components/create-status/create-status.component';
|
import { CreateStatusComponent } from './components/create-status/create-status.component';
|
||||||
import { MediaComponent } from './components/create-status/media/media.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 = [
|
const routes: Routes = [
|
||||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||||
|
@ -89,7 +95,12 @@ const routes: Routes = [
|
||||||
NotificationHubComponent,
|
NotificationHubComponent,
|
||||||
MediaViewerComponent,
|
MediaViewerComponent,
|
||||||
CreateStatusComponent,
|
CreateStatusComponent,
|
||||||
MediaComponent
|
MediaComponent,
|
||||||
|
MyAccountComponent,
|
||||||
|
FavoritesComponent,
|
||||||
|
DirectMessagesComponent,
|
||||||
|
MentionsComponent,
|
||||||
|
NotificationsComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
|
@ -102,7 +113,8 @@ const routes: Routes = [
|
||||||
NgxsModule.forRoot([
|
NgxsModule.forRoot([
|
||||||
RegisteredAppsState,
|
RegisteredAppsState,
|
||||||
AccountsState,
|
AccountsState,
|
||||||
StreamsState
|
StreamsState,
|
||||||
|
SettingsState
|
||||||
]),
|
]),
|
||||||
NgxsStoragePluginModule.forRoot()
|
NgxsStoragePluginModule.forRoot()
|
||||||
],
|
],
|
||||||
|
|
|
@ -129,7 +129,6 @@ describe('CreateStatusComponent', () => {
|
||||||
expect(result[1].length).toBeLessThanOrEqual(527);
|
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||||
expect(result[0]).toContain('@Lorem@ipsum.com ');
|
expect(result[0]).toContain('@Lorem@ipsum.com ');
|
||||||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||||
console.warn(result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
|
@ -10,7 +10,11 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"></app-manage-account>
|
<app-manage-account *ngIf="openPanel === 'manageAccount'"
|
||||||
|
[account]="userAccountUsed"
|
||||||
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)"></app-manage-account>
|
||||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
||||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||||
<app-search *ngIf="openPanel === 'search'"
|
<app-search *ngIf="openPanel === 'search'"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<p>
|
||||||
|
direct-messages works!
|
||||||
|
</p>
|
|
@ -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<DirectMessagesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ DirectMessagesComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DirectMessagesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<string>();
|
||||||
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<p>
|
||||||
|
favorites works!
|
||||||
|
</p>
|
|
@ -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<FavoritesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ FavoritesComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FavoritesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<string>();
|
||||||
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,53 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3 class="panel__title">Manage Account</h3>
|
<h3 class="panel__title">Manage Account</h3>
|
||||||
|
|
||||||
<div class="account-editor__display-avatar">
|
<div class="account__header">
|
||||||
<img class="account-editor__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " />
|
<img class="account__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " />
|
||||||
|
|
||||||
|
<!-- <a href class="account__header--button"><fa-icon [icon]="faUserPlus"></fa-icon></a> -->
|
||||||
|
<a href class="account__header--button" title="favorites" (click)="loadSubPanel('favorites')"
|
||||||
|
[ngClass]="{ 'account__header--button--selected': subPanel === 'favorites' }">
|
||||||
|
<fa-icon [icon]="faStar"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a href class="account__header--button" title="DM" (click)="loadSubPanel('dm')"
|
||||||
|
[ngClass]="{ 'account__header--button--selected': subPanel === 'dm' }">
|
||||||
|
<fa-icon [icon]="faEnvelope"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a href class="account__header--button" title="mentions" (click)="loadSubPanel('mentions')"
|
||||||
|
[ngClass]="{ 'account__header--button--selected': subPanel === 'mentions', 'account__header--button--notification': hasMentions }">
|
||||||
|
<fa-icon [icon]="faAt"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a href class="account__header--button" title="notifications" (click)="loadSubPanel('notifications')"
|
||||||
|
[ngClass]="{ 'account__header--button--selected': subPanel === 'notifications',
|
||||||
|
'account__header--button--notification': hasNotifications }">
|
||||||
|
<fa-icon [icon]="faBell"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a href class="account__header--button" title="account" (click)="loadSubPanel('account')"
|
||||||
|
[ngClass]="{ 'account__header--button--selected': subPanel === 'account' }">
|
||||||
|
<fa-icon [icon]="faUser"></fa-icon>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 class="account__label">add column:</h4>
|
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'"
|
||||||
|
[account]="account"
|
||||||
<a class="account__link account__blue" href *ngFor="let stream of availableStreams" (click)="addStream(stream)">
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
{{ stream.name }}
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
</a>
|
(browseThreadEvent)="browseThread($event)"></app-direct-messages>
|
||||||
<!-- <a class="add-column__link" href>
|
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'"
|
||||||
Global Timeline
|
[account]="account"
|
||||||
</a>
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
<a class="add-column__link" href>
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
Personnal Timeline
|
(browseThreadEvent)="browseThread($event)"></app-favorites>
|
||||||
</a>
|
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'"
|
||||||
<a class="add-column__link" href>
|
[account]="account"
|
||||||
Lists, Favs, Activitires, etc
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
</a> -->
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)"></app-mentions>
|
||||||
|
<app-my-account class="account__body" *ngIf="subPanel === 'account'"
|
||||||
<h4 class="account__label account__margin-top">remove account from sengi:</h4>
|
[account]="account"></app-my-account>
|
||||||
<a class="account__link account__red" href (click)="removeAccount()">
|
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'"
|
||||||
Delete
|
[account]="account"
|
||||||
</a>
|
(browseAccountEvent)="browseAccount($event)"
|
||||||
|
(browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)"></app-notifications>
|
||||||
</div>
|
</div>
|
|
@ -1,67 +1,60 @@
|
||||||
@import "variables";
|
@import "variables";
|
||||||
@import "panel";
|
@import "panel";
|
||||||
.account-editor {
|
@import "commons";
|
||||||
// padding: 10px 10px 0 7px;
|
$account-header-height: 60px;
|
||||||
// font-size: $small-font-size;
|
.panel {
|
||||||
// &__title {
|
padding-left: 0px;
|
||||||
// font-size: 13px;
|
padding-right: 0px;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
&__label {
|
&__header {
|
||||||
// text-decoration: underline;
|
// padding-left: 10px;
|
||||||
font-size: $small-font-size;
|
padding-left: 5px;
|
||||||
margin-left: 5px;
|
padding-right: 10px;
|
||||||
color: $font-color-secondary;
|
padding-bottom: 5px;
|
||||||
}
|
height: $account-header-height; //border-top: 1px solid #222736;
|
||||||
&__margin-top {
|
border-bottom: 1px solid #222736;
|
||||||
margin-top: 25px;
|
&--button {
|
||||||
}
|
// outline: 1px greenyellow solid;
|
||||||
&__link {
|
margin-top: 20px;
|
||||||
text-decoration: none;
|
width: 35px;
|
||||||
display: block; // width: calc(100% - 20px);
|
height: 35px;
|
||||||
width: 100%; // height: 30px;
|
float: right;
|
||||||
padding: 5px 10px; // border: solid 1px black;
|
margin-left: 5px;
|
||||||
&:not(:last-child) {
|
font-size: 22px;
|
||||||
margin-bottom: 5px;
|
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 {
|
&__avatar {
|
||||||
text-decoration: none;
|
width: 50px;
|
||||||
display: block; // width: calc(100% - 20px);
|
border-radius: 3px;
|
||||||
width: 45%; // height: 30px;
|
|
||||||
padding: 5px 10px; // border: solid 1px black;
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&__blue {
|
&__body {
|
||||||
background-color: $color-primary;
|
overflow: auto;
|
||||||
color: #fff;
|
height: calc(100% - #{$account-header-height} - 31px);
|
||||||
&:hover {
|
display: block;
|
||||||
background-color: lighten($color-primary, 15);
|
font-size: $default-font-size;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__red {
|
|
||||||
$red-button-color: rgb(65, 3, 3);
|
|
||||||
background-color: $red-button-color;
|
|
||||||
color: #fff;
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($red-button-color, 15);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,48 +1,85 @@
|
||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../states/streams.state';
|
import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Store } from '@ngxs/store';
|
import { faBell, faEnvelope, faUser, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { AccountsStateModel, AccountInfo, RemoveAccount } from '../../../states/accounts.state';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
import { AccountWrapper } from '../../../models/account.models';
|
import { AccountWrapper } from '../../../models/account.models';
|
||||||
import { NavigationService } from '../../../services/navigation.service';
|
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
|
||||||
import { NotificationService } from '../../../services/notification.service';
|
import { OpenThreadEvent } from '../../../services/tools.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manage-account',
|
selector: 'app-manage-account',
|
||||||
templateUrl: './manage-account.component.html',
|
templateUrl: './manage-account.component.html',
|
||||||
styleUrls: ['./manage-account.component.scss']
|
styleUrls: ['./manage-account.component.scss']
|
||||||
})
|
})
|
||||||
export class ManageAccountComponent implements OnInit {
|
export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||||
@Input() account: AccountWrapper;
|
faAt = faAt;
|
||||||
|
faBell = faBell;
|
||||||
|
faEnvelope = faEnvelope;
|
||||||
|
faUser = faUser;
|
||||||
|
faStar = faStar;
|
||||||
|
faUserPlus = faUserPlus;
|
||||||
|
|
||||||
availableStreams: StreamElement[] = [];
|
subPanel = 'account';
|
||||||
|
hasNotifications = false;
|
||||||
|
hasMentions = false;
|
||||||
|
|
||||||
|
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
@Input('account')
|
||||||
|
set account(acc: AccountWrapper) {
|
||||||
|
this._account = acc;
|
||||||
|
this.checkNotifications();
|
||||||
|
}
|
||||||
|
get account(): AccountWrapper {
|
||||||
|
return this._account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private userNotificationServiceSub: Subscription;
|
||||||
|
private _account: AccountWrapper;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly store: Store,
|
private readonly userNotificationService: UserNotificationService) { }
|
||||||
private readonly navigationService: NavigationService,
|
|
||||||
private notificationService: NotificationService) { }
|
|
||||||
|
|
||||||
ngOnInit() {
|
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 {
|
ngOnDestroy(): void {
|
||||||
if (stream) {
|
this.userNotificationServiceSub.unsubscribe();
|
||||||
this.store.dispatch([new AddStream(stream)]).toPromise()
|
}
|
||||||
.then(() => {
|
|
||||||
this.notificationService.notify(`stream added`, false);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAccount(): boolean {
|
browseAccount(accountName: string): void {
|
||||||
const accountId = this.account.info.id;
|
this.browseAccountEvent.next(accountName);
|
||||||
this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]);
|
}
|
||||||
this.navigationService.closePanel();
|
|
||||||
return false;
|
browseHashtag(hashtag: string): void {
|
||||||
|
this.browseHashtagEvent.next(hashtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||||
|
this.browseThreadEvent.next(openThreadEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<p>
|
||||||
|
mentions works!
|
||||||
|
</p>
|
|
@ -0,0 +1,7 @@
|
||||||
|
@import "variables";
|
||||||
|
@import "commons";
|
||||||
|
@import "mixins";
|
||||||
|
|
||||||
|
.stream-toots {
|
||||||
|
background-color: $column-background;
|
||||||
|
}
|
|
@ -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<MentionsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ MentionsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MentionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<string>();
|
||||||
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<div class="my-account__body flexcroll">
|
||||||
|
<h4 class="my-account__label">add column:</h4>
|
||||||
|
<a class="my-account__link my-account__blue" href *ngFor="let stream of availableStreams" (click)="addStream(stream)">
|
||||||
|
{{ stream.name }}
|
||||||
|
</a>
|
||||||
|
<h4 class="my-account__label my-account__margin-top">remove account from sengi:</h4>
|
||||||
|
<a class="my-account__link my-account__red" href (click)="removeAccount()">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<MyAccountComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ MyAccountComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyAccountComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<div class="stream flexcroll" #statusstream (scroll)="onScroll()">
|
||||||
|
<div class="stream__notification" *ngFor="let notification of notifications">
|
||||||
|
<!-- <div *ngIf="notification.type === 'favourite'">
|
||||||
|
<div class="stream__notification--icon">
|
||||||
|
<fa-icon class="favorite" [icon]="faStar"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stream__notification--label">
|
||||||
|
<a href class="stream__link">{{ notification.account.username }}</a> favorited your status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="notification.type === 'reblog'">
|
||||||
|
<div class="stream__notification--icon">
|
||||||
|
<fa-icon class="boost" [icon]="faRetweet"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stream__notification--label">
|
||||||
|
<a href class="stream__link">{{ notification.account.username }}</a> boosted your status
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div *ngIf="notification.type === 'follow'">
|
||||||
|
<div class="stream__notification--icon">
|
||||||
|
<fa-icon class="followed" [icon]="faUserPlus"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stream__notification--label">
|
||||||
|
<a href class="stream__link"
|
||||||
|
(click)="openAccount(notification.account)">{{ notification.account.display_name }}</a> followed
|
||||||
|
you!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href (click)="openAccount(notification.account)" class="follow-account" title="{{notification.account.acct}}">
|
||||||
|
<img class="follow-account__avatar" src="{{ notification.account.avatar }}" />
|
||||||
|
<span class="follow-account__display-name" >{{ notification.account.display_name }}</span>
|
||||||
|
<span class="follow-account__acct">@{{ notification.account.acct }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-status *ngIf="notification.status" class="stream__status" [statusWrapper]="notification.status"
|
||||||
|
[notificationAccount]="notification.account" [notificationType]="notification.type"
|
||||||
|
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
|
||||||
|
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<NotificationsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ NotificationsComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<string>();
|
||||||
|
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||||
|
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||||
|
|
||||||
|
@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';
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
<a class="account-icon"
|
<a class="account-icon"
|
||||||
href title="{{ account.info.id }}" (click)="toogleAccount()" (contextmenu)="openMenu()">
|
href title="{{ account.info.id }}" (click)="toogleAccount()" (contextmenu)="openMenu()">
|
||||||
<img class="account-icon__avatar" [class.account-icon__avatar--selected]="account.info.isSelected" src="{{ account.avatar }}" />
|
<span class="hasActivity" *ngIf="account.hasActivityNotifications">new</span>
|
||||||
|
<img class="account-icon__avatar" [class.account-icon__avatar--selected]="account.info.isSelected" src="{{ account.avatar }}" />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,45 +1,98 @@
|
||||||
.account-icon {
|
.account-icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 50px;
|
width: 50px; // padding-top: 4px;
|
||||||
// padding-top: 4px;
|
|
||||||
// margin-left: 5px;
|
// margin-left: 5px;
|
||||||
margin: 0 0 5px 5px;
|
margin: 0 0 5px 5px;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
border-radius: 50%;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
opacity: .3;
|
opacity: .3;
|
||||||
transition: all .2s;
|
transition: all .2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: alpha(opacity=50);
|
filter: alpha(opacity=50);
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
// border-radius: 20%;
|
// border-radius: 20%;
|
||||||
filter: alpha(opacity=100);
|
filter: alpha(opacity=100);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: alpha(opacity=100);
|
filter: alpha(opacity=100);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// & a {
|
@keyframes flickerAnimation {
|
||||||
// margin-left: 4px;
|
0% {
|
||||||
// /*margin-top: 4px;*/
|
opacity: 0;
|
||||||
// }
|
}
|
||||||
// & img {
|
50% {
|
||||||
// width: 40px;
|
opacity: 1;
|
||||||
// border-radius: 50%;
|
}
|
||||||
// }
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,28 +1,30 @@
|
||||||
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
||||||
|
|
||||||
import { AccountWrapper } from '../../../models/account.models';
|
import { AccountWrapper } from '../../../models/account.models';
|
||||||
|
import { AccountWithNotificationWrapper } from '../left-side-bar.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-account-icon',
|
selector: 'app-account-icon',
|
||||||
templateUrl: './account-icon.component.html',
|
templateUrl: './account-icon.component.html',
|
||||||
styleUrls: ['./account-icon.component.scss']
|
styleUrls: ['./account-icon.component.scss']
|
||||||
})
|
})
|
||||||
export class AccountIconComponent implements OnInit {
|
export class AccountIconComponent implements OnInit {
|
||||||
@Input() account: AccountWrapper;
|
@Input() account: AccountWithNotificationWrapper;
|
||||||
@Output() toogleAccountNotify = new EventEmitter<AccountWrapper>();
|
@Output() toogleAccountNotify = new EventEmitter<AccountWrapper>();
|
||||||
@Output() openMenuNotify = new EventEmitter<AccountWrapper>();
|
@Output() openMenuNotify = new EventEmitter<AccountWrapper>();
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
toogleAccount(): boolean {
|
toogleAccount(): boolean {
|
||||||
this.toogleAccountNotify.emit(this.account);
|
this.toogleAccountNotify.emit(this.account);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
openMenu(): boolean {
|
openMenu(): boolean {
|
||||||
this.openMenuNotify.emit(this.account);
|
this.openMenuNotify.emit(this.account);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { AccountInfo, SelectAccount } from "../../states/accounts.state";
|
||||||
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
||||||
import { MastodonService } from "../../services/mastodon.service";
|
import { MastodonService } from "../../services/mastodon.service";
|
||||||
import { NotificationService } from "../../services/notification.service";
|
import { NotificationService } from "../../services/notification.service";
|
||||||
|
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-left-side-bar",
|
selector: "app-left-side-bar",
|
||||||
|
@ -19,14 +20,15 @@ import { NotificationService } from "../../services/notification.service";
|
||||||
export class LeftSideBarComponent implements OnInit, OnDestroy {
|
export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
faCommentAlt = faCommentAlt;
|
faCommentAlt = faCommentAlt;
|
||||||
|
|
||||||
accounts: AccountWrapper[] = [];
|
accounts: AccountWithNotificationWrapper[] = [];
|
||||||
hasAccounts: boolean;
|
hasAccounts: boolean;
|
||||||
private accounts$: Observable<AccountInfo[]>;
|
private accounts$: Observable<AccountInfo[]>;
|
||||||
|
|
||||||
// private loadedAccounts: { [index: string]: AccountInfo } = {};
|
private accountSub: Subscription;
|
||||||
private sub: Subscription;
|
private notificationSub: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly userNotificationServiceService: UserNotificationService,
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly navigationService: NavigationService,
|
private readonly navigationService: NavigationService,
|
||||||
private readonly mastodonService: MastodonService,
|
private readonly mastodonService: MastodonService,
|
||||||
|
@ -37,7 +39,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private currentLoading: number;
|
private currentLoading: number;
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||||
if (accounts) {
|
if (accounts) {
|
||||||
//Update and Add
|
//Update and Add
|
||||||
for (let acc of accounts) {
|
for (let acc of accounts) {
|
||||||
|
@ -45,8 +47,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
if (previousAcc) {
|
if (previousAcc) {
|
||||||
previousAcc.info.isSelected = acc.isSelected;
|
previousAcc.info.isSelected = acc.isSelected;
|
||||||
} else {
|
} else {
|
||||||
const accWrapper = new AccountWrapper();
|
const accWrapper = new AccountWithNotificationWrapper();
|
||||||
accWrapper.info = acc;
|
accWrapper.info = acc;
|
||||||
|
|
||||||
this.accounts.push(accWrapper);
|
this.accounts.push(accWrapper);
|
||||||
|
|
||||||
this.mastodonService.retrieveAccountDetails(acc)
|
this.mastodonService.retrieveAccountDetails(acc)
|
||||||
|
@ -61,17 +64,31 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
//Delete
|
//Delete
|
||||||
const deletedAccounts = this.accounts.filter(x => accounts.findIndex(y => y.id === x.info.id) === -1);
|
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.accounts = this.accounts.filter(x => x.info.id !== delAcc.info.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasAccounts = this.accounts.length > 0;
|
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 {
|
ngOnDestroy(): void {
|
||||||
this.sub.unsubscribe();
|
this.accountSub.unsubscribe();
|
||||||
|
this.notificationSub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
onToogleAccountNotify(acc: AccountWrapper) {
|
onToogleAccountNotify(acc: AccountWrapper) {
|
||||||
|
@ -102,3 +119,14 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AccountWithNotificationWrapper extends AccountWrapper {
|
||||||
|
// constructor(accountWrapper: AccountWrapper) {
|
||||||
|
// super();
|
||||||
|
|
||||||
|
// this.avatar = accountWrapper.avatar;
|
||||||
|
// this.info = accountWrapper.info;
|
||||||
|
// }
|
||||||
|
|
||||||
|
hasActivityNotifications: boolean;
|
||||||
|
}
|
|
@ -119,10 +119,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||||
usableStatus
|
usableStatus
|
||||||
.then((status: Status) => {
|
.then((status: Status) => {
|
||||||
if (this.isBoosted) {
|
if (this.isBoosted && status.reblogged) {
|
||||||
return this.mastodonService.unreblog(account, status);
|
return this.mastodonService.unreblog(account, status);
|
||||||
} else {
|
} else if(!this.isBoosted && !status.reblogged){
|
||||||
return this.mastodonService.reblog(account, status);
|
return this.mastodonService.reblog(account, status);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((boostedStatus: Status) => {
|
.then((boostedStatus: Status) => {
|
||||||
|
@ -144,10 +146,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||||
usableStatus
|
usableStatus
|
||||||
.then((status: Status) => {
|
.then((status: Status) => {
|
||||||
if (this.isFavorited) {
|
if (this.isFavorited && status.favourited) {
|
||||||
return this.mastodonService.unfavorite(account, status);
|
return this.mastodonService.unfavorite(account, status);
|
||||||
} else {
|
} else if(!this.isFavorited && !status.favourited) {
|
||||||
return this.mastodonService.favorite(account, status);
|
return this.mastodonService.favorite(account, status);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((favoritedStatus: Status) => {
|
.then((favoritedStatus: Status) => {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a *ngIf="attachments.length === 3" class="galery__image--link galery__image--link-3-1"
|
<a *ngIf="attachments.length === 3" class="galery__image--link galery__image--link-3-1"
|
||||||
title="{{ attachments[0].text_url }}" (click)="attachmentSelected(0)">
|
title="{{ attachments[0].description }}" (click)="attachmentSelected(0)">
|
||||||
<img src="{{ attachments[0].preview_url }}" />
|
<img src="{{ attachments[0].preview_url }}" />
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="attachments.length === 3" class="galery__image--link galery__image--link-3-2"
|
<a *ngIf="attachments.length === 3" class="galery__image--link galery__image--link-3-2"
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a *ngIf="attachments.length === 4" class="galery__image--link galery__image--link-4"
|
<a *ngIf="attachments.length === 4" class="galery__image--link galery__image--link-4"
|
||||||
title="{{ attachments[0].text_url }}" (click)="attachmentSelected(0)">
|
title="{{ attachments[0].description }}" (click)="attachmentSelected(0)">
|
||||||
<img src="{{ attachments[0].preview_url }}" />
|
<img src="{{ attachments[0].preview_url }}" />
|
||||||
</a>
|
</a>
|
||||||
<a *ngIf="attachments.length === 4" class="galery__image--link galery__image--link-4"
|
<a *ngIf="attachments.length === 4" class="galery__image--link galery__image--link-4"
|
||||||
|
|
|
@ -2,53 +2,75 @@
|
||||||
<a class="reblog__profile-link" href (click)="openAccount(status.account)">{{ status.account.display_name }} <img
|
<a class="reblog__profile-link" href (click)="openAccount(status.account)">{{ status.account.display_name }} <img
|
||||||
*ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
|
*ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
|
||||||
</div>
|
</div>
|
||||||
<div class="status">
|
<div *ngIf="notificationType === 'favourite'">
|
||||||
|
<div class="notification--icon">
|
||||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
<fa-icon class="favorite" [icon]="faStar"></fa-icon>
|
||||||
(click)="openAccount(displayedStatus.account)">
|
|
||||||
<img [class.status__avatar--boosted]="reblog" class="status__avatar"
|
|
||||||
src="{{ displayedStatus.account.avatar }}" />
|
|
||||||
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
|
|
||||||
<span class="status__name">
|
|
||||||
<span class="status__name--displayname" innerHTML="{{displayedStatus.account.display_name}}"></span><span
|
|
||||||
class="status__name--username">{{displayedStatus.account.acct}}</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">
|
|
||||||
<a class="status__created-at--link" href="{{ displayedStatus.url }}" target="_blank">
|
|
||||||
{{ status.created_at | timeAgo | async }}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="status__labels">
|
<div class="notification--label">
|
||||||
<div class="status__labels--label status__labels--bot" title="bot" *ngIf="status.account.bot">
|
<a href class="notification--link"
|
||||||
bot
|
(click)="openAccount(notificationAccount)">{{ notificationAccount.display_name }}</a> favorited your status
|
||||||
</div>
|
|
||||||
<div class="status__labels--label status__labels--xpost" title="this status was cross-posted" *ngIf="isCrossPoster">
|
|
||||||
x-post
|
|
||||||
</div>
|
|
||||||
<div class="status__labels--label status__labels--thread" title="thread" *ngIf="isThread">
|
|
||||||
thread
|
|
||||||
</div>
|
|
||||||
<div class="status__labels--label status__labels--discuss" title="this status has a discution" *ngIf="hasReply">
|
|
||||||
replies
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
|
</div>
|
||||||
|
<div *ngIf="notificationType === 'reblog'">
|
||||||
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content"
|
<div class="notification--icon">
|
||||||
(click)="removeContentWarning()">
|
<fa-icon class="boost" [icon]="faRetweet"></fa-icon>
|
||||||
<span class="status__content-warning--title">sensitive content</span>
|
</div>
|
||||||
{{ contentWarningText }}
|
<div class="notification--label">
|
||||||
</a>
|
<a href class="notification--link" (click)="openAccount(notificationAccount)">{{ notificationAccount.display_name }}</a> boosted your status
|
||||||
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="displayedStatus.content"
|
</div>
|
||||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
</div>
|
||||||
(textSelected)="textSelected()"></app-databinded-text>
|
<div class="status">
|
||||||
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
|
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||||
[attachments]="displayedStatus.media_attachments">
|
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||||
</app-attachements>
|
(click)="openAccount(displayedStatus.account)">
|
||||||
|
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar"
|
||||||
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)"
|
src="{{ displayedStatus.account.avatar }}" />
|
||||||
(replyEvent)="openReply()"></app-action-bar>
|
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
|
||||||
|
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar }}" />
|
||||||
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()"></app-create-status>
|
<span class="status__name">
|
||||||
|
<span class="status__name--displayname"
|
||||||
|
innerHTML="{{displayedStatus.account.display_name}}"></span><span
|
||||||
|
class="status__name--username">{{displayedStatus.account.acct}}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">
|
||||||
|
<a class="status__created-at--link" href="{{ displayedStatus.url }}" target="_blank">
|
||||||
|
{{ status.created_at | timeAgo | async }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="status__labels">
|
||||||
|
<div class="status__labels--label status__labels--bot" title="bot" *ngIf="status.account.bot">
|
||||||
|
bot
|
||||||
|
</div>
|
||||||
|
<div class="status__labels--label status__labels--xpost" title="this status was cross-posted"
|
||||||
|
*ngIf="isCrossPoster">
|
||||||
|
x-post
|
||||||
|
</div>
|
||||||
|
<div class="status__labels--label status__labels--thread" title="thread" *ngIf="isThread">
|
||||||
|
thread
|
||||||
|
</div>
|
||||||
|
<div class="status__labels--label status__labels--discuss" title="this status has a discution"
|
||||||
|
*ngIf="hasReply">
|
||||||
|
replies
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
|
||||||
|
|
||||||
|
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content"
|
||||||
|
(click)="removeContentWarning()">
|
||||||
|
<span class="status__content-warning--title">sensitive content</span>
|
||||||
|
{{ contentWarningText }}
|
||||||
|
</a>
|
||||||
|
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="displayedStatus.content"
|
||||||
|
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||||
|
(textSelected)="textSelected()"></app-databinded-text>
|
||||||
|
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
|
||||||
|
[attachments]="displayedStatus.media_attachments">
|
||||||
|
</app-attachements>
|
||||||
|
|
||||||
|
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)"
|
||||||
|
(replyEvent)="openReply()"></app-action-bar>
|
||||||
|
</div>
|
||||||
|
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper"
|
||||||
|
(onClose)="closeReply()"></app-create-status>
|
||||||
</div>
|
</div>
|
|
@ -154,3 +154,52 @@
|
||||||
display: block; // width: calc(100% - 80px);
|
display: block; // width: calc(100% - 80px);
|
||||||
margin: 10px 10px 0 $avatar-column-space;
|
margin: 10px 10px 0 $avatar-column-space;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from "@angular/core";
|
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 { Status, Account } from "../../../services/models/mastodon.interfaces";
|
||||||
import { OpenThreadEvent } from "../../../services/tools.service";
|
import { OpenThreadEvent } from "../../../services/tools.service";
|
||||||
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
||||||
|
@ -10,6 +12,9 @@ import { StatusWrapper } from '../../../models/common.model';
|
||||||
styleUrls: ["./status.component.scss"]
|
styleUrls: ["./status.component.scss"]
|
||||||
})
|
})
|
||||||
export class StatusComponent implements OnInit {
|
export class StatusComponent implements OnInit {
|
||||||
|
faStar = faStar;
|
||||||
|
faRetweet = faRetweet;
|
||||||
|
|
||||||
displayedStatus: Status;
|
displayedStatus: Status;
|
||||||
reblog: boolean;
|
reblog: boolean;
|
||||||
hasAttachments: boolean;
|
hasAttachments: boolean;
|
||||||
|
@ -27,6 +32,9 @@ export class StatusComponent implements OnInit {
|
||||||
|
|
||||||
@Input() isThreadDisplay: boolean;
|
@Input() isThreadDisplay: boolean;
|
||||||
|
|
||||||
|
@Input() notificationType: 'mention' | 'reblog' | 'favourite';
|
||||||
|
@Input() notificationAccount: Account;
|
||||||
|
|
||||||
private _statusWrapper: StatusWrapper;
|
private _statusWrapper: StatusWrapper;
|
||||||
status: Status;
|
status: Status;
|
||||||
@Input('statusWrapper')
|
@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) {
|
if (status.in_reply_to_account_id && status.in_reply_to_account_id === status.account.id) {
|
||||||
this.isThread = true;
|
this.isThread = true;
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
||||||
|
<div class="stream-toots__remove-cw" *ngIf="isThread && hasContentWarnings">
|
||||||
|
<button class="stream-toots__remove-cw--button" (click)="removeCw()"
|
||||||
|
title="remove content warnings">Remove CWs</button>
|
||||||
|
</div>
|
||||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||||
|
|
||||||
<!-- data-simplebar -->
|
<!-- data-simplebar -->
|
||||||
|
|
|
@ -1,19 +1,49 @@
|
||||||
@import "variables";
|
@import "variables";
|
||||||
@import "commons";
|
@import "commons";
|
||||||
|
@import "mixins";
|
||||||
.stream-toots {
|
.stream-toots {
|
||||||
height: calc(100%);
|
height: calc(100%);
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
&__error {
|
&__error {
|
||||||
padding: 20px 20px 0 20px;
|
padding: 20px 20px 0 20px;
|
||||||
color: rgb(255, 113, 113);
|
color: rgb(255, 113, 113);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__status:not(:last-child) {
|
&__status:not(:last-child) {
|
||||||
border: solid #06070b;
|
border: solid #06070b;
|
||||||
border-width: 0 0 1px 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -21,6 +21,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
isThread = false;
|
isThread = false;
|
||||||
displayError: string;
|
displayError: string;
|
||||||
|
hasContentWarnings = false;
|
||||||
|
|
||||||
private _streamElement: StreamElement;
|
private _streamElement: StreamElement;
|
||||||
private account: AccountInfo;
|
private account: AccountInfo;
|
||||||
|
|
|
@ -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 { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
import { MastodonService } from '../../../services/mastodon.service';
|
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 { NotificationService } from '../../../services/notification.service';
|
||||||
import { AccountInfo } from '../../../states/accounts.state';
|
import { AccountInfo } from '../../../states/accounts.state';
|
||||||
import { StatusWrapper } from '../../../models/common.model';
|
import { StatusWrapper } from '../../../models/common.model';
|
||||||
|
import { StatusComponent } from '../status/status.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-thread',
|
selector: 'app-thread',
|
||||||
|
@ -18,6 +19,7 @@ export class ThreadComponent implements OnInit {
|
||||||
displayError: string;
|
displayError: string;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
isThread = true;
|
isThread = true;
|
||||||
|
hasContentWarnings = false;
|
||||||
|
|
||||||
private lastThreadEvent: OpenThreadEvent;
|
private lastThreadEvent: OpenThreadEvent;
|
||||||
|
|
||||||
|
@ -33,6 +35,8 @@ export class ThreadComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewChildren(StatusComponent) statusChildren: QueryList<StatusComponent>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
private readonly toolsService: ToolsService,
|
private readonly toolsService: ToolsService,
|
||||||
|
@ -86,6 +90,8 @@ export class ThreadComponent implements OnInit {
|
||||||
const wrapper = new StatusWrapper(s, currentAccount);
|
const wrapper = new StatusWrapper(s, currentAccount);
|
||||||
this.statuses.push(wrapper);
|
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 {
|
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||||
this.browseThreadEvent.next(openThreadEvent);
|
this.browseThreadEvent.next(openThreadEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeCw(){
|
||||||
|
const statuses = this.statusChildren.toArray();
|
||||||
|
statuses.forEach(x => {
|
||||||
|
x.removeContentWarning();
|
||||||
|
});
|
||||||
|
this.hasContentWarnings = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,9 +90,6 @@ export class UserProfileComponent implements OnInit {
|
||||||
|
|
||||||
return this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
|
return this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
|
||||||
.then((account: Account) => {
|
.then((account: Account) => {
|
||||||
|
|
||||||
console.warn(account);
|
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.statusLoading = true;
|
this.statusLoading = true;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Injectable } from '@angular/core';
|
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 { 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 { AccountInfo } from '../states/accounts.state';
|
||||||
import { StreamTypeEnum } from '../states/streams.state';
|
import { StreamTypeEnum } from '../states/streams.state';
|
||||||
|
|
||||||
|
@ -134,6 +134,27 @@ export class MastodonService {
|
||||||
return this.httpClient.get<Context>(route, { headers: headers }).toPromise();
|
return this.httpClient.get<Context>(route, { headers: headers }).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFavorites(account: AccountInfo, maxId: string = null): Promise<FavoriteResult> { //, 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<Status[]>) => {
|
||||||
|
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<Account[]> {
|
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false): Promise<Account[]> {
|
||||||
const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`;
|
const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`;
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
|
@ -152,20 +173,18 @@ export class MastodonService {
|
||||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
favorite(account: AccountInfo, status: Status): any {
|
favorite(account: AccountInfo, status: Status): Promise<Status> {
|
||||||
const route = `https://${account.instance}${this.apiRoutes.favouritingStatus}`.replace('{0}', status.id);
|
const route = `https://${account.instance}${this.apiRoutes.favouritingStatus}`.replace('{0}', status.id);
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
unfavorite(account: AccountInfo, status: Status): any {
|
unfavorite(account: AccountInfo, status: Status): Promise<Status> {
|
||||||
const route = `https://${account.instance}${this.apiRoutes.unfavouritingStatus}`.replace('{0}', status.id);
|
const route = `https://${account.instance}${this.apiRoutes.unfavouritingStatus}`.replace('{0}', status.id);
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise<Relationship[]> {
|
getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise<Relationship[]> {
|
||||||
let params = `?${this.formatArray(accountsToRetrieve.map(x => x.id.toString()), 'id')}`;
|
let params = `?${this.formatArray(accountsToRetrieve.map(x => x.id.toString()), 'id')}`;
|
||||||
|
|
||||||
|
@ -202,7 +221,7 @@ export class MastodonService {
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: add focus support
|
//TODO: add focus support
|
||||||
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
|
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
|
||||||
let input = new FormData();
|
let input = new FormData();
|
||||||
input.append('description', description);
|
input.append('description', description);
|
||||||
const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
|
const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
|
||||||
|
@ -210,10 +229,30 @@ export class MastodonService {
|
||||||
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
|
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNotifications(account: AccountInfo, excludeTypes: string[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
|
||||||
|
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<Notification[]>(route, { headers: headers }).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
private formatArray(data: string[], paramName: string): string {
|
private formatArray(data: string[], paramName: string): string {
|
||||||
let result = '';
|
let result = '';
|
||||||
data.forEach(x => {
|
data.forEach(x => {
|
||||||
if (result.includes('paramName')) result += '&';
|
if (result.includes(paramName)) result += '&';
|
||||||
result += `${paramName}[]=${x}`;
|
result += `${paramName}[]=${x}`;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
@ -236,3 +275,9 @@ class StatusData {
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
visibility: string;
|
visibility: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FavoriteResult {
|
||||||
|
constructor(
|
||||||
|
public max_id: string,
|
||||||
|
public favorites: Status[]) {}
|
||||||
|
}
|
|
@ -11,7 +11,12 @@ export interface TokenData {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
created_at: string;
|
created_at: number;
|
||||||
|
|
||||||
|
//TODO: Pleroma support this
|
||||||
|
me: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AccountInfo } from '../states/accounts.state';
|
||||||
import { MastodonService } from './mastodon.service';
|
import { MastodonService } from './mastodon.service';
|
||||||
import { Account, Results, Status } from "./models/mastodon.interfaces";
|
import { Account, Results, Status } from "./models/mastodon.interfaces";
|
||||||
import { StatusWrapper } from '../models/common.model';
|
import { StatusWrapper } from '../models/common.model';
|
||||||
|
import { AccountSettings, SaveAccountSettings } from '../states/settings.state';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -21,6 +22,23 @@ export class ToolsService {
|
||||||
return regAccounts.filter(x => x.isSelected);
|
return regAccounts.filter(x => x.isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAccountSettings(account: AccountInfo): AccountSettings {
|
||||||
|
var accountsSettings = <AccountSettings[]>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<Account> {
|
findAccount(account: AccountInfo, accountName: string): Promise<Account> {
|
||||||
return this.mastodonService.search(account, accountName, true)
|
return this.mastodonService.search(account, accountName, true)
|
||||||
.then((result: Results) => {
|
.then((result: Results) => {
|
||||||
|
@ -42,7 +60,7 @@ export class ToolsService {
|
||||||
if (!isProvider) {
|
if (!isProvider) {
|
||||||
statusPromise = statusPromise.then((foreignStatus: Status) => {
|
statusPromise = statusPromise.then((foreignStatus: Status) => {
|
||||||
const statusUrl = foreignStatus.url;
|
const statusUrl = foreignStatus.url;
|
||||||
return this.mastodonService.search(account, statusUrl)
|
return this.mastodonService.search(account, statusUrl, true)
|
||||||
.then((results: Results) => {
|
.then((results: Results) => {
|
||||||
return results.statuses[0];
|
return results.statuses[0];
|
||||||
});
|
});
|
||||||
|
@ -51,6 +69,7 @@ export class ToolsService {
|
||||||
|
|
||||||
return statusPromise;
|
return statusPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpenThreadEvent {
|
export class OpenThreadEvent {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<UserNotification[]>([]);
|
||||||
|
|
||||||
|
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<any>[] = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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<SettingsStateModel>({
|
||||||
|
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<SettingsStateModel>, 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<SettingsStateModel>, 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<SettingsStateModel>, action: SaveSettings){
|
||||||
|
const state = ctx.getState();
|
||||||
|
const newSettings = new GlobalSettings();
|
||||||
|
|
||||||
|
newSettings.disableAllNotifications = action.settings.disableAllNotifications;
|
||||||
|
newSettings.accountSettings = [...state.settings.accountSettings];
|
||||||
|
|
||||||
|
ctx.patchState({
|
||||||
|
settings: newSettings
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,9 +3,8 @@
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
height: calc(100%);
|
height: calc(100%);
|
||||||
padding: 10px 10px 0 7px;
|
padding: 10px 10px 0 7px;
|
||||||
font-size: $small-font-size;
|
font-size: $small-font-size; //FIXME: remove this
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
// overflow: auto;
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
|
@ -47,3 +47,7 @@ $button-color: darken(white, 30);
|
||||||
$button-color-hover: white;
|
$button-color-hover: white;
|
||||||
$button-background-color: $color-primary;
|
$button-background-color: $color-primary;
|
||||||
$button-background-color-hover: lighten($color-primary, 20);
|
$button-background-color-hover: lighten($color-primary, 20);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$column-background: #0f111a;
|
Loading…
Reference in New Issue