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",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"main": "main.js",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
|
|
@ -50,6 +50,12 @@ import { NotificationService } from "./services/notification.service";
|
|||
import { MediaViewerComponent } from './components/media-viewer/media-viewer.component';
|
||||
import { CreateStatusComponent } from './components/create-status/create-status.component';
|
||||
import { MediaComponent } from './components/create-status/media/media.component';
|
||||
import { MyAccountComponent } from './components/floating-column/manage-account/my-account/my-account.component';
|
||||
import { FavoritesComponent } from './components/floating-column/manage-account/favorites/favorites.component';
|
||||
import { DirectMessagesComponent } from './components/floating-column/manage-account/direct-messages/direct-messages.component';
|
||||
import { MentionsComponent } from './components/floating-column/manage-account/mentions/mentions.component';
|
||||
import { NotificationsComponent } from './components/floating-column/manage-account/notifications/notifications.component';
|
||||
import { SettingsState } from './states/settings.state';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
|
@ -89,7 +95,12 @@ const routes: Routes = [
|
|||
NotificationHubComponent,
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent
|
||||
MediaComponent,
|
||||
MyAccountComponent,
|
||||
FavoritesComponent,
|
||||
DirectMessagesComponent,
|
||||
MentionsComponent,
|
||||
NotificationsComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
|
@ -102,7 +113,8 @@ const routes: Routes = [
|
|||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
StreamsState,
|
||||
SettingsState
|
||||
]),
|
||||
NgxsStoragePluginModule.forRoot()
|
||||
],
|
||||
|
|
|
@ -129,7 +129,6 @@ describe('CreateStatusComponent', () => {
|
|||
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||
expect(result[0]).toContain('@Lorem@ipsum.com ');
|
||||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||
console.warn(result);
|
||||
});
|
||||
|
||||
});
|
|
@ -10,7 +10,11 @@
|
|||
</a>
|
||||
</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-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<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">
|
||||
<h3 class="panel__title">Manage Account</h3>
|
||||
|
||||
<div class="account-editor__display-avatar">
|
||||
<img class="account-editor__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " />
|
||||
</div>
|
||||
<div class="account__header">
|
||||
<img class="account__avatar" src="{{account.avatar}}" title="{{ account.info.id }} " />
|
||||
|
||||
<h4 class="account__label">add column:</h4>
|
||||
|
||||
<a class="account__link account__blue" href *ngFor="let stream of availableStreams" (click)="addStream(stream)">
|
||||
{{ stream.name }}
|
||||
<!-- <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 class="add-column__link" href>
|
||||
Global Timeline
|
||||
<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 class="add-column__link" href>
|
||||
Personnal Timeline
|
||||
<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 class="add-column__link" href>
|
||||
Lists, Favs, Activitires, etc
|
||||
</a> -->
|
||||
|
||||
|
||||
<h4 class="account__label account__margin-top">remove account from sengi:</h4>
|
||||
<a class="account__link account__red" href (click)="removeAccount()">
|
||||
Delete
|
||||
<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>
|
||||
|
||||
<app-direct-messages class="account__body" *ngIf="subPanel === 'dm'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-direct-messages>
|
||||
<app-favorites class="account__body" *ngIf="subPanel === 'favorites'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-favorites>
|
||||
<app-mentions class="account__body" *ngIf="subPanel === 'mentions'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-mentions>
|
||||
<app-my-account class="account__body" *ngIf="subPanel === 'account'"
|
||||
[account]="account"></app-my-account>
|
||||
<app-notifications class="account__body" *ngIf="subPanel === 'notifications'"
|
||||
[account]="account"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-notifications>
|
||||
</div>
|
|
@ -1,67 +1,60 @@
|
|||
@import "variables";
|
||||
@import "panel";
|
||||
.account-editor {
|
||||
// padding: 10px 10px 0 7px;
|
||||
// font-size: $small-font-size;
|
||||
// &__title {
|
||||
// font-size: 13px;
|
||||
// text-transform: uppercase;
|
||||
// margin: 6px 0 12px 0;
|
||||
// }
|
||||
&__display-avatar {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
&__avatar {
|
||||
// display: block;
|
||||
width: 75px;
|
||||
border-radius: 50px;
|
||||
transform: translateX(15px); // margin: auto;
|
||||
}
|
||||
@import "commons";
|
||||
$account-header-height: 60px;
|
||||
.panel {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.account {
|
||||
&__label {
|
||||
// text-decoration: underline;
|
||||
font-size: $small-font-size;
|
||||
&__header {
|
||||
// padding-left: 10px;
|
||||
padding-left: 5px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 5px;
|
||||
height: $account-header-height; //border-top: 1px solid #222736;
|
||||
border-bottom: 1px solid #222736;
|
||||
&--button {
|
||||
// outline: 1px greenyellow solid;
|
||||
margin-top: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
color: $font-color-secondary;
|
||||
}
|
||||
&__margin-top {
|
||||
margin-top: 25px;
|
||||
}
|
||||
&__link {
|
||||
text-decoration: none;
|
||||
display: block; // width: calc(100% - 20px);
|
||||
width: 100%; // height: 30px;
|
||||
padding: 5px 10px; // border: solid 1px black;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
&__mid-link {
|
||||
text-decoration: none;
|
||||
display: block; // width: calc(100% - 20px);
|
||||
width: 45%; // height: 30px;
|
||||
padding: 5px 10px; // border: solid 1px black;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
&__blue {
|
||||
background-color: $color-primary;
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
color: $font-link-primary;
|
||||
padding-left: 6px;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
background-color: lighten($color-primary, 15);
|
||||
color: $font-link-primary-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&__red {
|
||||
$red-button-color: rgb(65, 3, 3);
|
||||
background-color: $red-button-color;
|
||||
color: #fff;
|
||||
&--selected {
|
||||
color: whitesmoke;
|
||||
&:hover {
|
||||
background-color: lighten($red-button-color, 15);
|
||||
color: whitesmoke;
|
||||
}
|
||||
}
|
||||
&--notification {
|
||||
color: rgb(250, 152, 41);
|
||||
&:hover {
|
||||
color: rgb(255, 185, 106);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__avatar {
|
||||
width: 50px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&__body {
|
||||
overflow: auto;
|
||||
height: calc(100% - #{$account-header-height} - 31px);
|
||||
display: block;
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
}
|
|
@ -1,48 +1,85 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { StreamElement, StreamTypeEnum, AddStream, RemoveAllStreams } from '../../../states/streams.state';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { AccountsStateModel, AccountInfo, RemoveAccount } from '../../../states/accounts.state';
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBell, faEnvelope, faUser, faStar } from "@fortawesome/free-regular-svg-icons";
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AccountWrapper } from '../../../models/account.models';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { UserNotificationService, UserNotification } from '../../../services/user-notification.service';
|
||||
import { OpenThreadEvent } from '../../../services/tools.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-account',
|
||||
templateUrl: './manage-account.component.html',
|
||||
styleUrls: ['./manage-account.component.scss']
|
||||
})
|
||||
export class ManageAccountComponent implements OnInit {
|
||||
@Input() account: AccountWrapper;
|
||||
export class ManageAccountComponent implements OnInit, OnDestroy {
|
||||
faAt = faAt;
|
||||
faBell = faBell;
|
||||
faEnvelope = faEnvelope;
|
||||
faUser = faUser;
|
||||
faStar = faStar;
|
||||
faUserPlus = faUserPlus;
|
||||
|
||||
availableStreams: StreamElement[] = [];
|
||||
subPanel = 'account';
|
||||
hasNotifications = false;
|
||||
hasMentions = false;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<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(
|
||||
private readonly store: Store,
|
||||
private readonly navigationService: NavigationService,
|
||||
private notificationService: NotificationService) { }
|
||||
private readonly userNotificationService: UserNotificationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
const instance = this.account.info.instance;
|
||||
this.availableStreams.length = 0;
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, instance));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, instance));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, instance));
|
||||
|
||||
}
|
||||
|
||||
addStream(stream: StreamElement): boolean {
|
||||
if (stream) {
|
||||
this.store.dispatch([new AddStream(stream)]).toPromise()
|
||||
.then(() => {
|
||||
this.notificationService.notify(`stream added`, false);
|
||||
ngOnDestroy(): void {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
private checkNotifications(){
|
||||
if(this.userNotificationServiceSub){
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if(userNotification){
|
||||
this.hasNotifications = userNotification.hasNewNotifications;
|
||||
this.hasMentions = userNotification.hasNewMentions;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSubPanel(subpanel: string): boolean {
|
||||
this.subPanel = subpanel;
|
||||
return false;
|
||||
}
|
||||
|
||||
removeAccount(): boolean {
|
||||
const accountId = this.account.info.id;
|
||||
this.store.dispatch([new RemoveAllStreams(accountId), new RemoveAccount(accountId)]);
|
||||
this.navigationService.closePanel();
|
||||
return false;
|
||||
browseAccount(accountName: string): void {
|
||||
this.browseAccountEvent.next(accountName);
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.browseHashtagEvent.next(hashtag);
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
href title="{{ account.info.id }}" (click)="toogleAccount()" (contextmenu)="openMenu()">
|
||||
<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>
|
||||
|
|
|
@ -1,45 +1,98 @@
|
|||
.account-icon {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
// padding-top: 4px;
|
||||
width: 50px; // padding-top: 4px;
|
||||
// margin-left: 5px;
|
||||
margin: 0 0 5px 5px;
|
||||
|
||||
|
||||
|
||||
|
||||
&__avatar {
|
||||
border-radius: 50%;
|
||||
border-radius: 2px;
|
||||
width: 40px;
|
||||
opacity: .3;
|
||||
transition: all .2s;
|
||||
|
||||
&:hover {
|
||||
filter: alpha(opacity=50);
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
// border-radius: 20%;
|
||||
filter: alpha(opacity=100);
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
filter: alpha(opacity=100);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// & a {
|
||||
// margin-left: 4px;
|
||||
// /*margin-top: 4px;*/
|
||||
// }
|
||||
// & img {
|
||||
// width: 40px;
|
||||
// border-radius: 50%;
|
||||
// }
|
||||
@keyframes flickerAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes flickerAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes flickerAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes flickerAnimation {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hasActivity {
|
||||
|
||||
-webkit-animation: flickerAnimation 2s infinite;
|
||||
-moz-animation: flickerAnimation 2s infinite;
|
||||
-o-animation: flickerAnimation 2s infinite;
|
||||
animation: flickerAnimation 2s infinite;
|
||||
|
||||
border-radius: 2px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
border: 2px solid orange;
|
||||
z-index: 20;
|
||||
color: orange;
|
||||
font-size: 10px;
|
||||
font-style: italic;
|
||||
padding: 23px 0 0 3px;
|
||||
|
||||
background: rgba(0,0,0, .55);
|
||||
|
||||
&:hover {
|
||||
color: orange;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
|
||||
|
||||
import { AccountWrapper } from '../../../models/account.models';
|
||||
import { AccountWithNotificationWrapper } from '../left-side-bar.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-icon',
|
||||
|
@ -7,7 +9,7 @@ import { AccountWrapper } from '../../../models/account.models';
|
|||
styleUrls: ['./account-icon.component.scss']
|
||||
})
|
||||
export class AccountIconComponent implements OnInit {
|
||||
@Input() account: AccountWrapper;
|
||||
@Input() account: AccountWithNotificationWrapper;
|
||||
@Output() toogleAccountNotify = new EventEmitter<AccountWrapper>();
|
||||
@Output() openMenuNotify = new EventEmitter<AccountWrapper>();
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AccountInfo, SelectAccount } from "../../states/accounts.state";
|
|||
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
|
||||
import { MastodonService } from "../../services/mastodon.service";
|
||||
import { NotificationService } from "../../services/notification.service";
|
||||
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
|
||||
|
||||
@Component({
|
||||
selector: "app-left-side-bar",
|
||||
|
@ -19,14 +20,15 @@ import { NotificationService } from "../../services/notification.service";
|
|||
export class LeftSideBarComponent implements OnInit, OnDestroy {
|
||||
faCommentAlt = faCommentAlt;
|
||||
|
||||
accounts: AccountWrapper[] = [];
|
||||
accounts: AccountWithNotificationWrapper[] = [];
|
||||
hasAccounts: boolean;
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
|
||||
// private loadedAccounts: { [index: string]: AccountInfo } = {};
|
||||
private sub: Subscription;
|
||||
private accountSub: Subscription;
|
||||
private notificationSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly userNotificationServiceService: UserNotificationService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly mastodonService: MastodonService,
|
||||
|
@ -37,7 +39,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
|
||||
private currentLoading: number;
|
||||
ngOnInit() {
|
||||
this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
if (accounts) {
|
||||
//Update and Add
|
||||
for (let acc of accounts) {
|
||||
|
@ -45,8 +47,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
if (previousAcc) {
|
||||
previousAcc.info.isSelected = acc.isSelected;
|
||||
} else {
|
||||
const accWrapper = new AccountWrapper();
|
||||
const accWrapper = new AccountWithNotificationWrapper();
|
||||
accWrapper.info = acc;
|
||||
|
||||
this.accounts.push(accWrapper);
|
||||
|
||||
this.mastodonService.retrieveAccountDetails(acc)
|
||||
|
@ -68,10 +71,24 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
this.hasAccounts = this.accounts.length > 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => {
|
||||
|
||||
notifications.forEach((notification: UserNotification) => {
|
||||
const acc = this.accounts.find(x => x.info.id === notification.account.id);
|
||||
if(acc){
|
||||
acc.hasActivityNotifications = notification.hasNewMentions || notification.hasNewNotifications;
|
||||
}
|
||||
});
|
||||
|
||||
console.warn('new notifications');
|
||||
console.warn(notifications);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.sub.unsubscribe();
|
||||
this.accountSub.unsubscribe();
|
||||
this.notificationSub.unsubscribe();
|
||||
}
|
||||
|
||||
onToogleAccountNotify(acc: AccountWrapper) {
|
||||
|
@ -102,3 +119,14 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountWithNotificationWrapper extends AccountWrapper {
|
||||
// constructor(accountWrapper: AccountWrapper) {
|
||||
// super();
|
||||
|
||||
// this.avatar = accountWrapper.avatar;
|
||||
// this.info = accountWrapper.info;
|
||||
// }
|
||||
|
||||
hasActivityNotifications: boolean;
|
||||
}
|
|
@ -119,10 +119,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
if (this.isBoosted) {
|
||||
if (this.isBoosted && status.reblogged) {
|
||||
return this.mastodonService.unreblog(account, status);
|
||||
} else {
|
||||
} else if(!this.isBoosted && !status.reblogged){
|
||||
return this.mastodonService.reblog(account, status);
|
||||
} else {
|
||||
return Promise.resolve(status);
|
||||
}
|
||||
})
|
||||
.then((boostedStatus: Status) => {
|
||||
|
@ -144,10 +146,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
|||
const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper);
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
if (this.isFavorited) {
|
||||
if (this.isFavorited && status.favourited) {
|
||||
return this.mastodonService.unfavorite(account, status);
|
||||
} else {
|
||||
} else if(!this.isFavorited && !status.favourited) {
|
||||
return this.mastodonService.favorite(account, status);
|
||||
} else {
|
||||
return Promise.resolve(status);
|
||||
}
|
||||
})
|
||||
.then((favoritedStatus: Status) => {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</a>
|
||||
|
||||
<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 }}" />
|
||||
</a>
|
||||
<a *ngIf="attachments.length === 3" class="galery__image--link galery__image--link-3-2"
|
||||
|
@ -28,7 +28,7 @@
|
|||
</a>
|
||||
|
||||
<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 }}" />
|
||||
</a>
|
||||
<a *ngIf="attachments.length === 4" class="galery__image--link galery__image--link-4"
|
||||
|
|
|
@ -2,15 +2,34 @@
|
|||
<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
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'favourite'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="favorite" [icon]="faStar"></fa-icon>
|
||||
</div>
|
||||
<div class="notification--label">
|
||||
<a href class="notification--link"
|
||||
(click)="openAccount(notificationAccount)">{{ notificationAccount.display_name }}</a> favorited your status
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="notificationType === 'reblog'">
|
||||
<div class="notification--icon">
|
||||
<fa-icon class="boost" [icon]="faRetweet"></fa-icon>
|
||||
</div>
|
||||
<div class="notification--label">
|
||||
<a href class="notification--link" (click)="openAccount(notificationAccount)">{{ notificationAccount.display_name }}</a> boosted your status
|
||||
</div>
|
||||
</div>
|
||||
<div class="status">
|
||||
|
||||
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||
(click)="openAccount(displayedStatus.account)">
|
||||
<img [class.status__avatar--boosted]="reblog" class="status__avatar"
|
||||
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar"
|
||||
src="{{ displayedStatus.account.avatar }}" />
|
||||
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
|
||||
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar }}" />
|
||||
<span class="status__name">
|
||||
<span class="status__name--displayname" innerHTML="{{displayedStatus.account.display_name}}"></span><span
|
||||
<span class="status__name--displayname"
|
||||
innerHTML="{{displayedStatus.account.display_name}}"></span><span
|
||||
class="status__name--username">{{displayedStatus.account.acct}}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -23,13 +42,15 @@
|
|||
<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">
|
||||
<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">
|
||||
<div class="status__labels--label status__labels--discuss" title="this status has a discution"
|
||||
*ngIf="hasReply">
|
||||
replies
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,6 +70,7 @@
|
|||
|
||||
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)"
|
||||
(replyEvent)="openReply()"></app-action-bar>
|
||||
|
||||
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()"></app-create-status>
|
||||
</div>
|
||||
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper"
|
||||
(onClose)="closeReply()"></app-create-status>
|
||||
</div>
|
|
@ -154,3 +154,52 @@
|
|||
display: block; // width: calc(100% - 80px);
|
||||
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 { faStar, faRetweet } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Status, Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { OpenThreadEvent } from "../../../services/tools.service";
|
||||
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
||||
|
@ -10,6 +12,9 @@ import { StatusWrapper } from '../../../models/common.model';
|
|||
styleUrls: ["./status.component.scss"]
|
||||
})
|
||||
export class StatusComponent implements OnInit {
|
||||
faStar = faStar;
|
||||
faRetweet = faRetweet;
|
||||
|
||||
displayedStatus: Status;
|
||||
reblog: boolean;
|
||||
hasAttachments: boolean;
|
||||
|
@ -27,6 +32,9 @@ export class StatusComponent implements OnInit {
|
|||
|
||||
@Input() isThreadDisplay: boolean;
|
||||
|
||||
@Input() notificationType: 'mention' | 'reblog' | 'favourite';
|
||||
@Input() notificationAccount: Account;
|
||||
|
||||
private _statusWrapper: StatusWrapper;
|
||||
status: Status;
|
||||
@Input('statusWrapper')
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<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>
|
||||
|
||||
<!-- data-simplebar -->
|
||||
|
|
|
@ -1,19 +1,49 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
@import "mixins";
|
||||
.stream-toots {
|
||||
height: calc(100%);
|
||||
width: calc(100%);
|
||||
|
||||
overflow: auto;
|
||||
|
||||
&__error {
|
||||
padding: 20px 20px 0 20px;
|
||||
color: rgb(255, 113, 113);
|
||||
}
|
||||
|
||||
&__status:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
&__remove-cw {
|
||||
padding: 5px;
|
||||
// border: solid #06070b;
|
||||
// border-width: 0 0 1px 0;
|
||||
height: 45px;
|
||||
// width: calc(100%);
|
||||
// position: relative;
|
||||
|
||||
&--button {
|
||||
@include clearButton;
|
||||
|
||||
// position: absolute;
|
||||
// width: calc(80%);
|
||||
// margin-left: 40%;
|
||||
// transform: translateX(-40%);
|
||||
|
||||
width: calc(100%);
|
||||
padding: 5px 0;
|
||||
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
border: 3px $status-secondary-color double;
|
||||
transition: all .2s;
|
||||
background-color: $color-secondary;
|
||||
|
||||
&:hover{
|
||||
$hover-color: $status-secondary-color;
|
||||
background-color: $hover-color;
|
||||
color: white;
|
||||
border: 3px $hover-color double;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
|
|||
isLoading = true;
|
||||
isThread = false;
|
||||
displayError: string;
|
||||
hasContentWarnings = false;
|
||||
|
||||
private _streamElement: StreamElement;
|
||||
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 { MastodonService } from '../../../services/mastodon.service';
|
||||
|
@ -7,6 +7,7 @@ import { Results, Context, Status } from '../../../services/models/mastodon.inte
|
|||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
import { StatusComponent } from '../status/status.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thread',
|
||||
|
@ -18,6 +19,7 @@ export class ThreadComponent implements OnInit {
|
|||
displayError: string;
|
||||
isLoading = true;
|
||||
isThread = true;
|
||||
hasContentWarnings = false;
|
||||
|
||||
private lastThreadEvent: OpenThreadEvent;
|
||||
|
||||
|
@ -33,6 +35,8 @@ export class ThreadComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewChildren(StatusComponent) statusChildren: QueryList<StatusComponent>;
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
|
@ -86,6 +90,8 @@ export class ThreadComponent implements OnInit {
|
|||
const wrapper = new StatusWrapper(s, currentAccount);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
|
||||
this.hasContentWarnings = this.statuses.filter(x => x.status.sensitive || x.status.spoiler_text).length > 1;
|
||||
});
|
||||
|
||||
})
|
||||
|
@ -119,4 +125,12 @@ export class ThreadComponent implements OnInit {
|
|||
browseThread(openThreadEvent: OpenThreadEvent): void {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
|
||||
removeCw(){
|
||||
const statuses = this.statusChildren.toArray();
|
||||
statuses.forEach(x => {
|
||||
x.removeContentWarning();
|
||||
});
|
||||
this.hasContentWarnings = false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,9 +90,6 @@ export class UserProfileComponent implements OnInit {
|
|||
|
||||
return this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
|
||||
.then((account: Account) => {
|
||||
|
||||
console.warn(account);
|
||||
|
||||
this.isLoading = false;
|
||||
this.statusLoading = true;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpHeaders, HttpClient } from '@angular/common/http';
|
||||
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
|
||||
|
||||
import { ApiRoutes } from './models/api.settings';
|
||||
import { Account, Status, Results, Context, Relationship, Instance, Attachment } from "./models/mastodon.interfaces";
|
||||
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification } from "./models/mastodon.interfaces";
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
import { StreamTypeEnum } from '../states/streams.state';
|
||||
|
||||
|
@ -134,6 +134,27 @@ export class MastodonService {
|
|||
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[]> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
|
@ -152,20 +173,18 @@ export class MastodonService {
|
|||
return this.httpClient.post<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 headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
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 headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
|
||||
|
||||
getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise<Relationship[]> {
|
||||
let params = `?${this.formatArray(accountsToRetrieve.map(x => x.id.toString()), 'id')}`;
|
||||
|
||||
|
@ -210,10 +229,30 @@ export class MastodonService {
|
|||
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 {
|
||||
let result = '';
|
||||
data.forEach(x => {
|
||||
if (result.includes('paramName')) result += '&';
|
||||
if (result.includes(paramName)) result += '&';
|
||||
result += `${paramName}[]=${x}`;
|
||||
});
|
||||
return result;
|
||||
|
@ -236,3 +275,9 @@ class StatusData {
|
|||
spoiler_text: string;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export class FavoriteResult {
|
||||
constructor(
|
||||
public max_id: string,
|
||||
public favorites: Status[]) {}
|
||||
}
|
|
@ -11,7 +11,12 @@ export interface TokenData {
|
|||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
created_at: string;
|
||||
created_at: number;
|
||||
|
||||
//TODO: Pleroma support this
|
||||
me: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AccountInfo } from '../states/accounts.state';
|
|||
import { MastodonService } from './mastodon.service';
|
||||
import { Account, Results, Status } from "./models/mastodon.interfaces";
|
||||
import { StatusWrapper } from '../models/common.model';
|
||||
import { AccountSettings, SaveAccountSettings } from '../states/settings.state';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -21,6 +22,23 @@ export class ToolsService {
|
|||
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> {
|
||||
return this.mastodonService.search(account, accountName, true)
|
||||
.then((result: Results) => {
|
||||
|
@ -42,7 +60,7 @@ export class ToolsService {
|
|||
if (!isProvider) {
|
||||
statusPromise = statusPromise.then((foreignStatus: Status) => {
|
||||
const statusUrl = foreignStatus.url;
|
||||
return this.mastodonService.search(account, statusUrl)
|
||||
return this.mastodonService.search(account, statusUrl, true)
|
||||
.then((results: Results) => {
|
||||
return results.statuses[0];
|
||||
});
|
||||
|
@ -51,6 +69,7 @@ export class ToolsService {
|
|||
|
||||
return statusPromise;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class OpenThreadEvent {
|
||||
|
|
|
@ -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%);
|
||||
height: calc(100%);
|
||||
padding: 10px 10px 0 7px;
|
||||
font-size: $small-font-size;
|
||||
font-size: $small-font-size; //FIXME: remove this
|
||||
white-space: normal;
|
||||
// overflow: auto;
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -47,3 +47,7 @@ $button-color: darken(white, 30);
|
|||
$button-color-hover: white;
|
||||
$button-background-color: $color-primary;
|
||||
$button-background-color-hover: lighten($color-primary, 20);
|
||||
|
||||
|
||||
|
||||
$column-background: #0f111a;
|
Loading…
Reference in New Issue