Merge pull request #160 from NicolasConstant/develop

0.16.0 Release PR
This commit is contained in:
Nicolas Constant 2019-09-07 19:06:35 -04:00 committed by GitHub
commit c69ff3dd3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1236 additions and 138 deletions

7
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.14.0",
"version": "0.15.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -8185,6 +8185,11 @@
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
"dev": true
},
"ng-pick-datetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ng-pick-datetime/-/ng-pick-datetime-7.0.0.tgz",
"integrity": "sha512-SbS+zKX6gOlYpgH8zDSx2EL32ak0Z0y1Ksu1ECP/FiwVBM2mHgbzdfyDYhMmKFB0GKn5yCwXTandR1FCQXe62w=="
},
"ngx-contextmenu": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ngx-contextmenu/-/ngx-contextmenu-5.2.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.15.0",
"version": "0.16.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
@ -48,6 +48,7 @@
"bootstrap": "^4.1.3",
"core-js": "^2.5.4",
"emojione": "~4.5.0",
"ng-pick-datetime": "^7.0.0",
"ngx-contextmenu": "^5.2.0",
"rxjs": "^6.4.0",
"tslib": "^1.9.0",

View File

@ -1,4 +1,5 @@
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule } from "@angular/forms";
import { HttpModule } from "@angular/http";
import { HttpClientModule } from '@angular/common/http';
@ -14,6 +15,7 @@ import { OverlayModule } from '@angular/cdk/overlay';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { ContextMenuModule } from 'ngx-contextmenu';
import { PickerModule } from '@ctrl/ngx-emoji-mart';
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime';
import { AppComponent } from "./app.component";
import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.component";
@ -68,6 +70,11 @@ import { TimeLeftPipe } from './pipes/time-left.pipe';
import { AutosuggestComponent } from './components/create-status/autosuggest/autosuggest.component';
import { EmojiPickerComponent } from './components/create-status/emoji-picker/emoji-picker.component';
import { StatusUserContextMenuComponent } from './components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component';
import { StatusSchedulerComponent } from './components/create-status/status-scheduler/status-scheduler.component';
import { PollEditorComponent } from './components/create-status/poll-editor/poll-editor.component';
import { PollEntryComponent } from './components/create-status/poll-editor/poll-entry/poll-entry.component';
import { ScheduledStatusesComponent } from './components/floating-column/scheduled-statuses/scheduled-statuses.component';
import { ScheduledStatusComponent } from './components/floating-column/scheduled-statuses/scheduled-status/scheduled-status.component';
const routes: Routes = [
@ -122,7 +129,12 @@ const routes: Routes = [
TimeLeftPipe,
AutosuggestComponent,
EmojiPickerComponent,
StatusUserContextMenuComponent
StatusUserContextMenuComponent,
StatusSchedulerComponent,
PollEditorComponent,
PollEntryComponent,
ScheduledStatusesComponent,
ScheduledStatusComponent
],
entryComponents: [
EmojiPickerComponent
@ -130,10 +142,13 @@ const routes: Routes = [
imports: [
FontAwesomeModule,
BrowserModule,
BrowserAnimationsModule,
HttpModule,
HttpClientModule,
FormsModule,
PickerModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
RouterModule.forRoot(routes),

View File

@ -73,20 +73,22 @@ export class AutosuggestComponent implements OnInit, OnDestroy {
if (isAccount) {
for (let account of results.accounts) {
if (account.acct != this.lastPatternUsed) {
//if (account.acct != this.lastPatternUsed) {
this.accounts.push(new SelectableAccount(account));
this.accounts[0].selected = true;
if (this.accounts.length > 7) return;
}
//}
}
}
else {
for (let hashtag of results.hashtags) {
if (hashtag.includes(this.lastPatternUsed) && hashtag !== this.lastPatternUsed) {
//if (hashtag !== this.lastPatternUsed) {
//if (hashtag.includes(this.lastPatternUsed.toLocaleLowerCase()) && hashtag !== this.lastPatternUsed) {
//if (hashtag.includes(this.lastPatternUsed) && hashtag !== this.lastPatternUsed) {
this.hashtags.push(new SelectableHashtag(hashtag));
this.hashtags[0].selected = true;
if (this.hashtags.length > 7) return;
}
//}
}
}
})
@ -98,7 +100,7 @@ export class AutosuggestComponent implements OnInit, OnDestroy {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
}

View File

@ -21,13 +21,19 @@
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
</app-autosuggest>
<app-poll-editor *ngIf="pollIsActive"></app-poll-editor>
<app-status-scheduler class="scheduler" *ngIf="scheduleIsActive"></app-status-scheduler>
<div class="status-editor__footer" #footer>
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
<span *ngIf="!isSending">REPLY!</span>
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
<span *ngIf="!isSending">POST!</span>
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<div class="status-editor__footer__counter">
@ -47,6 +53,14 @@
<fa-icon [icon]="faLock" *ngIf="selectedPrivacy === 'Follows-only'"></fa-icon>
<fa-icon [icon]="faEnvelope" *ngIf="selectedPrivacy === 'DM'"></fa-icon>
</a>
<a href class="status-editor__footer--link status-editor__footer--add-poll" title="add poll" (click)="addPoll()">
<fa-icon [icon]="faPollH"></fa-icon>
</a>
<a href class="status-editor__footer--link" title="schedule" (click)="schedule()">
<fa-icon [icon]="faClock"></fa-icon>
</a>
</div>
<context-menu #contextMenu>

View File

@ -128,6 +128,13 @@ $counter-width: 90px;
margin: 2px 0 0 5px;
}
&--add-poll {
font-size: 16px;
margin: 0 0 0 5px;
position: relative;
top: 0px;
}
&--send-button {
@include clearButton;
transition: all .2s;
@ -183,11 +190,12 @@ $counter-width: 90px;
}
.emojipicker {
font-size: $default-font-size !important;
}
.scheduler {
display: block;
margin: 0 5px;
}
@import '~@angular/cdk/overlay-prebuilt.css';

View File

@ -3,11 +3,11 @@ import { HttpErrorResponse } from '@angular/common/http';
import { Store } from '@ngxs/store';
import { Subscription, Observable } from 'rxjs';
import { UP_ARROW, DOWN_ARROW, ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { faPaperclip, faGlobe, faGlobeAmericas, faLock, faLockOpen, faEnvelope } from "@fortawesome/free-solid-svg-icons";
import { faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons";
import { faPaperclip, faGlobe, faGlobeAmericas, faLock, faLockOpen, faEnvelope, faPollH } from "@fortawesome/free-solid-svg-icons";
import { faClock, faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons";
import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
import { MastodonService, VisibilityEnum } from '../../services/mastodon.service';
import { MastodonService, VisibilityEnum, PollParameters } from '../../services/mastodon.service';
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
import { ToolsService } from '../../services/tools.service';
import { NotificationService } from '../../services/notification.service';
@ -19,6 +19,9 @@ import { AutosuggestSelection, AutosuggestUserActionEnum } from './autosuggest/a
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
import { EmojiPickerComponent } from './emoji-picker/emoji-picker.component';
import { PollEditorComponent } from './poll-editor/poll-editor.component';
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
import { ScheduledStatusService } from '../../services/scheduled-status.service';
@Component({
selector: 'app-create-status',
@ -32,6 +35,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
faLock = faLock;
faLockOpen = faLockOpen;
faEnvelope = faEnvelope;
faPollH = faPollH;
faClock = faClock;
autoSuggestUserActionsStream = new EventEmitter<AutosuggestUserActionEnum>();
@ -89,7 +94,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, value.provider);
})
.then(() => {
this.isSending = false;
@ -113,6 +118,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
@ViewChild('fileInput') fileInputElement: ElementRef;
@ViewChild('footer') footerElement: ElementRef;
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
private _isDirectMention: boolean;
@Input('isDirectMention')
@ -148,6 +155,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private selectedAccount: AccountInfo;
constructor(
private readonly scheduledStatusService: ScheduledStatusService,
private readonly contextMenuService: ContextMenuService,
private readonly store: Store,
private readonly notificationService: NotificationService,
@ -280,7 +288,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.countStatusChar(this.status);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.selectedAccount);
});
}
@ -296,7 +304,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.setVisibility(defaultPrivacy);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.selectedAccount);
});
}
@ -432,17 +440,35 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
usableStatus = Promise.resolve(null);
}
let poll: PollParameters = null;
if (this.pollIsActive) {
poll = this.pollEditor.getPollParameters();
}
let scheduledTime = null;
if(this.scheduleIsActive){
scheduledTime = this.statusScheduler.getScheduledDate();
if(!scheduledTime || scheduledTime === '') {
this.isSending = false;
return;
}
}
usableStatus
.then((status: Status) => {
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments);
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
})
.then((res: Status) => {
this.title = '';
this.status = '';
this.onClose.emit();
if(this.scheduleIsActive){
this.scheduledStatusService.statusAdded(acc);
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, acc);
})
.then(() => {
this.isSending = false;
@ -451,7 +477,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return false;
}
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[]): Promise<Status> {
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
let parsedStatus = this.parseStatus(status);
let resultPromise = Promise.resolve(previousStatus);
@ -465,13 +492,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
.then((status: Status) => {
this.mediaService.clearMedia();
return status;
});
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, []);
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
}
})
.then((status: Status) => {
@ -611,7 +638,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
let scrolling = (this.replyElement.nativeElement.scrollHeight);
if (scrolling > 110) {
const isVisible = this.checkVisible(this.footerElement.nativeElement);
const isVisible = this.checkVisible(this.footerElement.nativeElement);
//this.replyElement.nativeElement.style.height = `0px`;
this.replyElement.nativeElement.style.height = `${this.replyElement.nativeElement.scrollHeight}px`;
@ -705,4 +732,16 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.overlayRef.dispose();
return false;
}
pollIsActive: boolean;
addPoll(): boolean {
this.pollIsActive = !this.pollIsActive;
return false;
}
scheduleIsActive: boolean;
schedule(): boolean {
this.scheduleIsActive = !this.scheduleIsActive;
return false;
}
}

View File

@ -40,7 +40,7 @@ export class EmojiPickerComponent implements OnInit {
this.customEmojis = emojis.map(x => this.convertEmoji(x));
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, currentAccount);
})
.then(() => {
this.loaded = true;

View File

@ -0,0 +1,20 @@
<div class="poll-editor">
<div class="poll-editor__entries">
<div *ngFor="let e of entries">
<app-poll-entry class="poll-editor__entry" [entry]="e" (removeEvent)="removeElement(e)"
(toogleMultiEvent)="toogleMulti()"></app-poll-entry>
</div>
</div>
<div class="poll-editor__footer">
<select [(ngModel)]="selectedId" class="poll-editor__footer--select-duration">
<option *ngFor="let d of delayChoice" [ngValue]="d.id">{{d.label}}</option>
</select>
<a href (click)="addEntry()" class="poll-editor__footer--add-choice">
<fa-icon [icon]="faPlus"></fa-icon> Add a choice
</a>
</div>
</div>

View File

@ -0,0 +1,37 @@
@import "variables";
.poll-editor {
background-color: $poll-editor-background;
border-top: 1px solid $poll-editor-separator;
min-height: 30px;
margin: 0 5px;
&__entries {
padding: 2px 0;
}
&__entry {
display: block;
}
&__footer {
transition: all .2s;
border-top: 1px solid $poll-editor-separator;
min-height: 30px;
padding: 5px;
&--add-choice {
color: rgb(49, 49, 49);
padding: 0 5px 0 5px;
&:hover {
text-decoration: none;
color: rgb(122, 122, 122);
}
}
&--select-duration {
float: right;
}
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollEditorComponent } from './poll-editor.component';
describe('PollEditorComponent', () => {
let component: PollEditorComponent;
let fixture: ComponentFixture<PollEditorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PollEditorComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,80 @@
import { Component, OnInit } from '@angular/core';
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { PollEntry } from './poll-entry/poll-entry.component';
import { PollParameters } from '../../../services/mastodon.service';
import { retry } from 'rxjs/operators';
@Component({
selector: 'app-poll-editor',
templateUrl: './poll-editor.component.html',
styleUrls: ['./poll-editor.component.scss']
})
export class PollEditorComponent implements OnInit {
faPlus = faPlus;
private entryUuid: number = 0;
entries: PollEntry[] = [];
delayChoice: Delay[] = [];
selectedId: string;
private multiSelected: boolean;
constructor() {
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
this.delayChoice.push(new Delay(60 * 5, "5 minutes"));
this.delayChoice.push(new Delay(60 * 30, "30 minutes"));
this.delayChoice.push(new Delay(60 * 60, "1 hour"));
this.delayChoice.push(new Delay(60 * 60 * 6, "6 hours"));
this.delayChoice.push(new Delay(60 * 60 * 24, "1 day"));
this.delayChoice.push(new Delay(60 * 60 * 24 * 3, "3 days"));
this.delayChoice.push(new Delay(60 * 60 * 24 * 7, "7 days"));
this.delayChoice.push(new Delay(60 * 60 * 24 * 15, "15 days"));
this.delayChoice.push(new Delay(60 * 60 * 24 * 30, "30 days"));
this.selectedId = this.delayChoice[4].id;
}
ngOnInit() {
}
private getEntryUuid(): number {
this.entryUuid++;
return this.entryUuid;
}
addEntry(): boolean {
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
return false;
}
removeElement(entry: PollEntry){
this.entries = this.entries.filter(x => x.id != entry.id);
}
toogleMulti() {
this.multiSelected = !this.multiSelected;
this.entries.forEach((e: PollEntry) => {
e.isMulti = this.multiSelected;
});
}
getPollParameters(): PollParameters {
let params = new PollParameters();
params.expires_in = this.delayChoice.find(x => x.id === this.selectedId).delayInSeconds;
params.multiple = this.multiSelected;
params.options = this.entries.map(x => x.label);
params.hide_totals = false;
return params;
}
}
class Delay {
constructor(public delayInSeconds: number, public label: string) {
this.id = delayInSeconds.toString();
}
id: string;
}

View File

@ -0,0 +1,16 @@
<div class="poll-entry">
<div class="poll-entry__remove">
<a href (click)="remove()" title="remove" class="poll-entry__remove--link">
<fa-icon [icon]="faTimes"></fa-icon>
</a>
</div>
<div class="poll-entry__multi">
<a href (click)="toogleMulti()" class="poll-entry__multi--link">
<span class="check-mark" [class.check-mark__round]="!entry.isMulti" [class.check-mark__box]="entry.isMulti">
</span>
</a>
</div>
<div class="poll-entry__label">
<input type="text" [(ngModel)]="entry.label" class="poll-entry__label--input" [(ngModel)]="entry.label"/>
</div>
</div>

View File

@ -0,0 +1,66 @@
@import "variables";
$selector-size: 20px;
$selector-padding: 5px;
.poll-entry {
position: relative;
&__multi {
&--link {
display: block;
padding: $selector-padding;
width: calc(#{$selector-size} + 2 * #{$selector-padding});
float: left;
}
}
&__label {
height: calc(#{$selector-size} + 2 * #{$selector-padding});
padding-top: 3px;
&--input {
width: calc(100% - #{$selector-size} - 2 * #{$selector-padding} - 30px);
border:1px solid $poll-editor-input-border;
&:focus {
outline: none !important;
border:1px solid $poll-editor-input-border-focus;
box-shadow: 0 0 0 #fff;
}
}
}
&__remove {
float: right;
width: 25px;
height: 30px;
&--link {
position: absolute;
top: -2px;
right: 5px;
color: rgb(139, 139, 139);
padding: 5px;
&:hover {
color: rgb(0, 0, 0);
}
}
}
}
.check-mark {
display: block;
border: 1px solid rgb(100, 100, 100);
width: $selector-size;
height: $selector-size;
&__round {
border-radius: 50px;
}
&__box {
border-radius: 3px;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollEntryComponent } from './poll-entry.component';
describe('PollEntryComponent', () => {
let component: PollEntryComponent;
let fixture: ComponentFixture<PollEntryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PollEntryComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { faTimes } from "@fortawesome/free-solid-svg-icons";
@Component({
selector: 'app-poll-entry',
templateUrl: './poll-entry.component.html',
styleUrls: ['./poll-entry.component.scss']
})
export class PollEntryComponent implements OnInit {
faTimes = faTimes;
@Input() entry: PollEntry;
@Output() removeEvent = new EventEmitter();
@Output() toogleMultiEvent = new EventEmitter();
constructor() { }
ngOnInit() {
}
remove(): boolean {
this.removeEvent.next();
return false;
}
toogleMulti(): boolean {
this.toogleMultiEvent.next();
return false;
}
}
export class PollEntry {
constructor(public id: number, public isMulti: boolean) {
}
public label: string;
}

View File

@ -0,0 +1,5 @@
<div class="scheduler">
<input class="scheduler__input" [owlDateTime]="dt2" [owlDateTimeTrigger]="dt2" placeholder="" [min]="min" [(ngModel)]="scheduledDate">
<a class="scheduler__icon" href (click)="openScheduler()" [owlDateTimeTrigger]="dt2" title="open datetime picker"><fa-icon [icon]="faCalendarAlt"></fa-icon></a>
<owl-date-time #dt2></owl-date-time>
</div>

View File

@ -0,0 +1,28 @@
@import "variables";
.scheduler {
background-color: $scheduler-background;
//margin: 0 5px;
border-bottom: 1px solid whitesmoke;
&__input {
color: whitesmoke;
padding: 3px;
width: calc(100% - 25px);
border: 1px solid $scheduler-background;
outline: 0;
background-color: $scheduler-background;
&:focus{
border: 1px solid $scheduler-background;
outline: 0;
}
}
&__icon {
color: whitesmoke;
&:hover {
color:rgb(204, 204, 204);
}
margin-left: 5px;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StatusSchedulerComponent } from './status-scheduler.component';
describe('StatusSchedulerComponent', () => {
let component: StatusSchedulerComponent;
let fixture: ComponentFixture<StatusSchedulerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StatusSchedulerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatusSchedulerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, OnInit, Input } from '@angular/core';
import { faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
@Component({
selector: 'app-status-scheduler',
templateUrl: './status-scheduler.component.html',
styleUrls: ['./status-scheduler.component.scss']
})
export class StatusSchedulerComponent implements OnInit {
faCalendarAlt = faCalendarAlt;
min = new Date();
// scheduledDate: string;
@Input() scheduledDate: string;
constructor() { }
ngOnInit() {
}
openScheduler(): boolean {
return false;
}
getScheduledDate(): string {
try {
return new Date(this.scheduledDate).toISOString();
} catch(err){
return null;
}
}
}

View File

@ -69,11 +69,11 @@ export class AddNewAccountComponent implements OnInit {
})
.catch((err: HttpErrorResponse) => {
if (err instanceof HttpErrorResponse) {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, null);
} else if ((<Error>err).message === 'CORS') {
this.notificationService.notify('Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
this.notificationService.notify(null, null, 'Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
} else {
this.notificationService.notify('Unkown error', true);
this.notificationService.notify(null, null, 'Unkown error', true);
}
});

View File

@ -24,6 +24,7 @@
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)">
</app-search>
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
<app-scheduled-statuses *ngIf="openPanel === 'scheduledStatuses'"></app-scheduled-statuses>
</div>
</div>
</div>

View File

@ -13,7 +13,7 @@ import { StatusWrapper } from '../../models/common.model';
styleUrls: ['./floating-column.component.scss']
})
export class FloatingColumnComponent implements OnInit, OnDestroy {
faTimes = faTimes;
overlayActive: boolean;
overlayAccountToBrowse: string;
@ -85,6 +85,13 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
this.openPanel = 'settings';
}
break;
case LeftPanelType.ScheduledStatuses:
if (this.openPanel === 'scheduledStatuses') {
this.closePanel();
} else {
this.openPanel = 'scheduledStatuses';
}
break;
default:
this.openPanel = '';
}
@ -92,7 +99,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
if(this.activatedPanelSub) {
if (this.activatedPanelSub) {
this.activatedPanelSub.unsubscribe();
}
}

View File

@ -65,7 +65,7 @@ export class DirectMessagesComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
@ -100,7 +100,7 @@ export class DirectMessagesComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;

View File

@ -65,7 +65,7 @@ export class FavoritesComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;
@ -102,7 +102,7 @@ export class FavoritesComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;

View File

@ -67,7 +67,7 @@ export class ManageAccountComponent implements OnInit, OnDestroy {
this.userAccount = acc;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
});
}

View File

@ -118,7 +118,7 @@ export class MentionsComponent implements OnInit, OnDestroy {
this.lastId = result[result.length - 1].id;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;

View File

@ -38,7 +38,7 @@ export class ListEditorComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
});
}
@ -57,7 +57,7 @@ export class ListEditorComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
});
}
@ -89,7 +89,7 @@ export class ListEditorComponent implements OnInit {
this.accountsInList.push(accountWrapper);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
accountWrapper.isLoading = false;
@ -131,7 +131,7 @@ export class ListEditorComponent implements OnInit {
this.accountsInList = this.accountsInList.filter(x => x.account.id !== accountWrapper.account.id);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
accountWrapper.isLoading = false;

View File

@ -115,7 +115,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
});
}
@ -149,7 +149,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
this.availableLists.push(wrappedStream);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.creationLoading = false;
@ -178,7 +178,7 @@ export class MyAccountComponent implements OnInit, OnDestroy {
this.availableLists = this.availableLists.filter(x => x.id !== list.id);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
});
return false;

View File

@ -119,7 +119,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
this.lastId = notifications[notifications.length - 1].id;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account.info);
})
.then(() => {
this.isLoading = false;

View File

@ -0,0 +1,49 @@
<div class="scheduled-status">
<div class="scheduled-status__date">
{{ status.scheduled_at | date: 'MMM d, y, h:mm a' }}
</div>
<div class="scheduled-status__avatar">
<img class="scheduled-status__avatar--image" src="{{avatar}}" />
</div>
<div class="scheduled-status__content">
<div class="scheduled-status__content--text scheduled-status__content--spoiler"
*ngIf="status.params.spoiler_text" title="spoiler">
{{ status.params.spoiler_text }}
</div>
<div class="scheduled-status__content--text" title="status text">
{{ status.params.text }}
</div>
</div>
<div class="scheduled-status__edition">
<div *ngIf="!deleting && !rescheduling">
<button class="scheduled-status__edition--button" (click)="delete()" title="delete status">Delete</button>
<button class="scheduled-status__edition--button" (click)="reschedule()"
title="reschedule status">Reschedule</button>
</div>
<div *ngIf="deleting">
<button class="scheduled-status__edition--button" (click)="cancelDeletion()" title="cancel">CANCEL</button>
<button class="scheduled-status__edition--button scheduled-status__edition--delete"
(click)="confirmDeletion()" title="confirm status deletion">DO IT</button>
<div class="scheduled-status__edition--label">
Delete the status?
</div>
</div>
<div *ngIf="rescheduling">
<app-status-scheduler [scheduledDate]="status.scheduled_at" class="scheduled-status__edition--scheduler" #statusScheduler></app-status-scheduler>
<button class="scheduled-status__edition--button" (click)="cancelReschedule()" title="cancel">CANCEL</button>
<button class="scheduled-status__edition--button"
(click)="confirmReschedule()" title="confirm rescheduling">REPLAN</button>
</div>
<app-waiting-animation class="waiting-icon" *ngIf="isLoading"></app-waiting-animation>
</div>
</div>

View File

@ -0,0 +1,74 @@
@import "commons";
@import "variables";
@import "mixins";
$avatar-size: 40px;
.scheduled-status {
margin: 0 5px;
padding: 5px 5px 5px 5px;
&__date {
margin-bottom: 1px;
}
&__avatar {
float: left;
&--image {
width: $avatar-size;
}
}
&__content {
&--text {
width: calc(100% - #{$avatar-size});
margin-left: $avatar-size;
padding: 0 5px;
min-height: 40px;
}
&--spoiler {
color: gray;
min-height: 0px;
}
}
&__edition {
@include clearfix;
&--button {
@include clearButton;
transition: all .2s;
float: right;
margin: 5px 0 5px 5px;
padding: 5px 10px;
background-color: #273047;
background-color: $scheduler-background;
&:hover {
background-color: #3b4769;
background-color: #3d4b7c;
}
}
&--delete {
background-color: rgb(95, 5, 5);
&:hover {
background-color: rgb(163, 4, 4);
}
}
&--label{
height: 30px;
float: right;
padding: 9px 5px 0 5px;
}
&--scheduler {
display: block;
margin: 5px 0 0 0;
}
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduledStatusComponent } from './scheduled-status.component';
describe('ScheduledStatusComponent', () => {
let component: ScheduledStatusComponent;
let fixture: ComponentFixture<ScheduledStatusComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ScheduledStatusComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ScheduledStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,97 @@
import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core';
import { AccountInfo } from '../../../../states/accounts.state';
import { ScheduledStatus } from '../../../../services/models/mastodon.interfaces';
import { ToolsService } from '../../../../services/tools.service';
import { MastodonService } from '../../../../services/mastodon.service';
import { NotificationService } from '../../../../services/notification.service';
import { ScheduledStatusService } from '../../../../services/scheduled-status.service';
import { StatusSchedulerComponent } from '../../../../components/create-status/status-scheduler/status-scheduler.component';
@Component({
selector: 'app-scheduled-status',
templateUrl: './scheduled-status.component.html',
styleUrls: ['./scheduled-status.component.scss']
})
export class ScheduledStatusComponent implements OnInit {
deleting: boolean = false;
rescheduling: boolean = false;
isLoading: boolean = false;
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
avatar: string;
@Input() account: AccountInfo;
@Input() status: ScheduledStatus;
@Output() rescheduledEvent = new EventEmitter();
constructor(
private readonly scheduledStatusService: ScheduledStatusService,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
ngOnInit() {
this.toolsService.getAvatar(this.account)
.then((avatar: string) => {
this.avatar = avatar;
});
}
delete(): boolean {
this.deleting = !this.deleting;
return false;
}
cancelDeletion(): boolean {
this.deleting = false;
return false;
}
confirmDeletion(): boolean {
if(this.isLoading) return false;
this.isLoading = true;
this.mastodonService.deleteScheduledStatus(this.account, this.status.id)
.then(() => {
this.scheduledStatusService.removeStatus(this.account, this.status.id);
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
});
return false;
}
reschedule(): boolean {
this.rescheduling = !this.rescheduling;
return false;
}
cancelReschedule(): boolean {
this.rescheduling = false;
return false;
}
confirmReschedule(): boolean {
if(this.isLoading) return false;
this.isLoading = true;
let scheduledTime = this.statusScheduler.getScheduledDate();
this.mastodonService.changeScheduledStatus(this.account, this.status.id, scheduledTime)
.then(() => {
this.status.scheduled_at = scheduledTime;
this.rescheduling = false;
this.rescheduledEvent.next();
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
});
return false;
}
}

View File

@ -0,0 +1,12 @@
<div class="panel">
<h3 class="panel__title">Scheduled Statuses</h3>
<div class="scheduled-statuses-display flexcroll">
<div *ngFor="let n of scheduledStatuses" class="scheduled-status">
<app-scheduled-status
(rescheduledEvent)="statusRescheduled()"
[account]="n.account"
[status]="n.status"></app-scheduled-status>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
@import "variables";
@import "panel";
@import "commons";
.panel {
padding-left: 0px;
padding-right: 0px;
}
.scheduled-statuses-display {
overflow: auto;
height: calc(100% - #{$stream-header-height});
}
.scheduled-status {
display: block;
// outline: 1px dotted salmon;
&:not(:last-child) {
border-bottom: 1px solid #141824;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduledStatusesComponent } from './scheduled-statuses.component';
xdescribe('ScheduledStatusesComponent', () => {
let component: ScheduledStatusesComponent;
let fixture: ComponentFixture<ScheduledStatusesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ScheduledStatusesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ScheduledStatusesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,54 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { ScheduledStatusService, ScheduledStatusNotification } from '../../../services/scheduled-status.service';
import { ScheduledStatus } from '../../../services/models/mastodon.interfaces';
import { AccountInfo } from '../../../states/accounts.state';
@Component({
selector: 'app-scheduled-statuses',
templateUrl: './scheduled-statuses.component.html',
styleUrls: ['./scheduled-statuses.component.scss']
})
export class ScheduledStatusesComponent implements OnInit, OnDestroy {
private statusSub: Subscription;
scheduledStatuses: ScheduledStatusWrapper[] = [];
constructor(
private readonly scheduledStatusService: ScheduledStatusService) {
}
ngOnInit() {
this.statusSub = this.scheduledStatusService.scheduledStatuses.subscribe((value: ScheduledStatusNotification[]) => {
this.scheduledStatuses.length = 0;
value.forEach(notification => {
notification.statuses.forEach(status => {
let wrapper = new ScheduledStatusWrapper(notification.account, status);
this.scheduledStatuses.push(wrapper);
});
});
this.sortStatuses();
});
}
ngOnDestroy(): void {
if (this.statusSub) this.statusSub.unsubscribe();
}
private sortStatuses() {
this.scheduledStatuses.sort((x, y) => new Date(x.status.scheduled_at).getTime() - new Date(y.status.scheduled_at).getTime());
}
statusRescheduled() {
this.sortStatuses();
}
}
class ScheduledStatusWrapper {
constructor(
public readonly account: AccountInfo,
public status: ScheduledStatus) {
}
}

View File

@ -82,7 +82,7 @@ export class SearchComponent implements OnInit {
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.lastAccountUsed);
})
.then(() => { this.isLoading = false; });
}

View File

@ -45,7 +45,7 @@ export class AccountIconComponent implements OnInit {
window.open(account.url, '_blank');
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, null);
});
return false;
}

View File

@ -19,7 +19,15 @@
<fa-icon [icon]="faPlus"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-link" href title="settings" (click)="openSettings()"
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
*ngIf="hasAccounts && hasScheduledStatuses"
(click)="openScheduledStatuses()"
(contextmenu)="openScheduledStatuses()">
<fa-icon [icon]="faCalendarAlt"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
(contextmenu)="openSettings()" *ngIf="hasAccounts">
<fa-icon [icon]="faCog"></fa-icon>
</a>

View File

@ -42,17 +42,24 @@ $height-button: 40px;
padding: 2px 0 5px 18px;
font-size: 14px;
}
&--scheduled {
font-size: 24px;
position: absolute;
left: 14px;
bottom: 37px;
}
&--cog {
font-size: 24px;
padding: 5px 0 0 12px;
position: absolute;
bottom: 7px;
}
&--bottom {
opacity: .3;
transition: all .3s;
filter: alpha(opacity=30);
// color: darken($font-link-primary, 30);
&:hover{
filter: alpha(opacity=100);
opacity: 1;

View File

@ -1,17 +1,15 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { HttpErrorResponse } from "@angular/common/http";
import { Subscription, Observable } from "rxjs";
import { Store } from "@ngxs/store";
import { faPlus, faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import { faCommentAlt } from "@fortawesome/free-regular-svg-icons";
import { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
import { Account } from "../../services/models/mastodon.interfaces";
import { AccountWrapper } from "../../models/account.models";
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';
import { ToolsService } from '../../services/tools.service';
import { ScheduledStatusService, ScheduledStatusNotification } from '../../services/scheduled-status.service';
@Component({
selector: "app-left-side-bar",
@ -21,28 +19,29 @@ import { UserNotificationService, UserNotification } from '../../services/user-n
export class LeftSideBarComponent implements OnInit, OnDestroy {
faCommentAlt = faCommentAlt;
faSearch = faSearch;
faPlus = faPlus;
faPlus = faPlus;
faCog = faCog;
faCalendarAlt = faCalendarAlt;
accounts: AccountWithNotificationWrapper[] = [];
hasAccounts: boolean;
hasScheduledStatuses: boolean;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
private scheduledSub: Subscription;
private notificationSub: Subscription;
constructor(
private readonly scheduledStatusService: ScheduledStatusService,
private readonly toolsService: ToolsService,
private readonly userNotificationServiceService: UserNotificationService,
private readonly notificationService: NotificationService,
private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService,
private readonly store: Store) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
private currentLoading: number;
ngOnInit() {
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (accounts) {
@ -57,12 +56,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.accounts.push(accWrapper);
this.mastodonService.retrieveAccountDetails(acc)
.then((result: Account) => {
accWrapper.avatar = result.avatar;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.toolsService.getAvatar(acc)
.then((avatar: string) => {
accWrapper.avatar = avatar;
});
}
}
@ -80,11 +76,25 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
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){
if (acc) {
acc.hasActivityNotifications = notification.hasNewMentions || notification.hasNewNotifications;
}
});
});
this.scheduledSub = this.scheduledStatusService.scheduledStatuses.subscribe((notifications: ScheduledStatusNotification[]) => {
console.warn(notifications);
let statuses = [];
notifications.forEach(n => {
n.statuses.forEach(x => {
statuses.push(x);
})
})
this.hasScheduledStatuses = statuses.length > 0;
console.warn(`hasScheduledStatuses ${this.hasScheduledStatuses}`);
});
}
ngOnDestroy(): void {
@ -119,15 +129,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.navigationService.openPanel(LeftPanelType.Settings);
return false;
}
openScheduledStatuses(): boolean {
this.navigationService.openPanel(LeftPanelType.ScheduledStatuses);
return false;
}
}
export class AccountWithNotificationWrapper extends AccountWrapper {
// constructor(accountWrapper: AccountWrapper) {
// super();
// this.avatar = accountWrapper.avatar;
// this.info = accountWrapper.info;
// }
hasActivityNotifications: boolean;
}

View File

@ -1,5 +1,13 @@
<div class="notification-hub">
<div class="notification-hub__notification" [ngClass]="{'notification-hub__notification--error':notification.isError}" *ngFor="let notification of notifications" (click)="onClick(notification)" title="close">
{{ notification.message }}
<div class="notification-hub__notification"
[ngClass]="{'notification-hub__notification--error':notification.isError}"
*ngFor="let notification of notifications" (click)="onClick(notification)" title="close">
<img class="notification-hub__notification--avatar" *ngIf="notification.avatar"
src="{{ notification.avatar }}" />
<div class="notification-hub__notification--message">
<span *ngIf="!notification.message">Error {{ notification.errorCode }}</span>
<span *ngIf="notification.message">{{ notification.message }}</span>
</div>
</div>
</div>
</div>

View File

@ -7,10 +7,11 @@
&__notification{
background-color: #22b90e;
color: black;
padding: 5px 10px;
padding: 5px 10px 5px 5px;
border-radius: 2px;
margin: 0 0 5px 15px;
max-width: 305px;
min-height: 40px;
white-space: pre-line;
word-wrap: break-word;
cursor: pointer;
@ -19,5 +20,14 @@
background-color: #be0a0a;
color: whitesmoke;
}
&--avatar {
width: 30px;
float: left;
}
&--message {
margin-left: 37px;
}
}
}

View File

@ -23,13 +23,13 @@ export class NotificationHubComponent implements OnInit {
//this.autoSubmit();
}
autoSubmit(): any {
this.notificationService.notify("test message", true);
// autoSubmit(): any {
// //this.notificationService.notify("test message", true);
setTimeout(() => {
this.autoSubmit();
}, 1500);
}
// setTimeout(() => {
// this.autoSubmit();
// }, 1500);
// }
onClick(notification: NotificatioData): void{
this.notifications = this.notifications.filter(x => x.id !== notification.id);

View File

@ -161,7 +161,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.checkIfBoosted();
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
})
.then(() => {
this.boostIsLoading = false;
@ -192,7 +192,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
// this.isFavorited = !this.isFavorited;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
})
.then(() => {
this.favoriteIsLoading = false;

View File

@ -139,7 +139,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, acc);
});
});
@ -157,7 +157,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, acc);
});
});
@ -175,7 +175,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;
@ -192,7 +192,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;
@ -210,7 +210,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;
@ -227,7 +227,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;
@ -249,7 +249,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
this.notificationService.deleteStatus(deletedStatus);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, selectedAccount);
});
return false;

View File

@ -99,7 +99,7 @@ export class PollComponent implements OnInit {
return poll;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, newSelectedAccount);
return null;
});
} else if (this.statusWrapper.status.visibility !== 'public' && this.statusWrapper.status.visibility !== 'unlisted' && this.statusWrapper.provider.id !== newSelectedAccount.id) {
@ -109,7 +109,7 @@ export class PollComponent implements OnInit {
.then((poll: Poll) => {
this.poll = poll;
})
.catch(err => this.notificationService.notifyHttpError(err));
.catch(err => this.notificationService.notifyHttpError(err, newSelectedAccount));
}
this.selectedAccount = newSelectedAccount;
}
@ -127,7 +127,7 @@ export class PollComponent implements OnInit {
this.poll = poll;
this.pollPerAccountId[selectedAccount.id] = Promise.resolve(poll);
})
.catch(err => this.notificationService.notifyHttpError(err));
.catch(err => this.notificationService.notifyHttpError(err, selectedAccount));
return false;
}
@ -152,7 +152,7 @@ export class PollComponent implements OnInit {
this.poll = poll;
this.pollPerAccountId[selectedAccount.id] = Promise.resolve(poll);
})
.catch(err => this.notificationService.notifyHttpError(err));
.catch(err => this.notificationService.notifyHttpError(err, selectedAccount));
return false;
}

View File

@ -259,7 +259,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account);
})
.then(() => {
this.isLoading = false;
@ -287,7 +287,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.account);
});
}

View File

@ -179,7 +179,7 @@ export class ThreadComponent implements OnInit, OnDestroy {
}, 0);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, currentAccount);
})
.then(() => {
this.isLoading = false;

View File

@ -42,7 +42,7 @@
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div class="profile__moved" *ngIf="displayedAccount && displayedAccount.moved">
{{displayedAccount | accountEmoji }} has moved to <a href
<span innerHTML="{{displayedAccount | accountEmoji }}"></span> has moved to <br/><a href
(click)="openMigratedAccount(displayedAccount.moved)" class="profile__moved--link"
title="open @{{displayedAccount.moved.acct }}">@{{displayedAccount.moved.acct }}</a>
</div>

View File

@ -106,7 +106,7 @@ export class UserProfileComponent implements OnInit {
return this.getFollowStatus(userAccount, account);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.loadingRelationShip = false;
@ -170,7 +170,7 @@ export class UserProfileComponent implements OnInit {
return Promise.all([getFollowStatusPromise, getStatusesPromise, getPinnedStatusesPromise]);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, this.currentlyUsedAccount);
})
.then(() => {
this.isLoading = false;
@ -188,7 +188,7 @@ export class UserProfileComponent implements OnInit {
}
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
});
}
@ -200,7 +200,7 @@ export class UserProfileComponent implements OnInit {
this.loadStatus(userAccount, statuses);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.statusLoading = false;
@ -214,7 +214,7 @@ export class UserProfileComponent implements OnInit {
this.relationship = result.filter(x => x.id === account.id)[0];
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.loadingRelationShip = false;
@ -277,7 +277,7 @@ export class UserProfileComponent implements OnInit {
this.relationship = relationship;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
});
return false;
}
@ -292,7 +292,7 @@ export class UserProfileComponent implements OnInit {
this.relationship = relationship;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, userAccount);
});
return false;
}

View File

@ -58,7 +58,7 @@ export class RegisterNewAccountComponent implements OnInit {
var instance = appDataWrapper.instance.toLowerCase();
if(this.isAccountAlreadyPresent(username, instance)){
this.notificationService.notify(`Account @${username}@${instance} is already registered`, true);
this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
this.router.navigate(['/home']);
return;
}
@ -75,7 +75,7 @@ export class RegisterNewAccountComponent implements OnInit {
});
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, null);
});
});
}

View File

@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
@Injectable()
export class MastodonService {
export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { }
@ -84,12 +84,14 @@ export class MastodonService {
return origString.replace(regEx, "");
};
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[]): Promise<Status> {
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> {
const url = `https://${account.instance}${this.apiRoutes.postNewStatus}`;
const statusData = new StatusData();
statusData.status = status;
statusData.media_ids = mediaIds;
statusData.poll = poll;
statusData.scheduled_at = scheduled_at;
if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id;
@ -385,6 +387,25 @@ export class MastodonService {
let route = `https://${account.instance}${this.apiRoutes.getCustomEmojis}`;
return this.httpClient.get<Emoji[]>(route).toPromise();
}
getScheduledStatuses(account: AccountInfo): Promise<ScheduledStatus[]> {
let route = `https://${account.instance}${this.apiRoutes.getScheduledStatuses}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<ScheduledStatus[]>(route, { headers: headers }).toPromise();
}
changeScheduledStatus(account: AccountInfo, statusId: string, scheduled_at: string): Promise<ScheduledStatus>{
let route = `https://${account.instance}${this.apiRoutes.putScheduleStatus}`.replace('{0}', statusId);
route = `${route}?scheduled_at=${scheduled_at}`
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.put<ScheduledStatus>(route, null, { headers: headers }).toPromise();
}
deleteScheduledStatus(account: AccountInfo, statusId: string): Promise<any> {
let route = `https://${account.instance}${this.apiRoutes.deleteScheduleStatus}`.replace('{0}', statusId);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.delete<ScheduledStatus>(route, { headers: headers }).toPromise();
}
}
export enum VisibilityEnum {
@ -397,11 +418,20 @@ export enum VisibilityEnum {
class StatusData {
status: string;
media_ids: string[];
in_reply_to_id: string;
media_ids: string[];
poll: PollParameters;
sensitive: boolean;
spoiler_text: string;
visibility: string;
scheduled_at: string;
}
export class PollParameters {
options: string [] = [];
expires_in: number;
multiple: boolean;
hide_totals: boolean;
}
export class FavoriteResult {

View File

@ -45,7 +45,7 @@ export class MediaService {
})
.catch((err) => {
this.remove(wrapper);
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
});
}
@ -60,7 +60,7 @@ export class MediaService {
this.mediaSubject.next(medias);
})
.catch((err) => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
});
}
@ -94,7 +94,7 @@ export class MediaService {
})
.catch((err) => {
this.remove(media);
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
});
}
}

View File

@ -67,4 +67,7 @@ export class ApiRoutes {
voteOnPoll = '/api/v1/polls/{0}/votes';
getPoll = '/api/v1/polls/{0}';
getConversations = '/api/v1/conversations';
getScheduledStatuses = '/api/v1/scheduled_statuses';
putScheduleStatus = '/api/v1/scheduled_statuses/{0}';
deleteScheduleStatus = '/api/v1/scheduled_statuses/{0}';
}

View File

@ -228,4 +228,22 @@ export interface Poll {
export interface PollOption {
title: string;
votes_count: number;
}
export interface ScheduledStatus {
id: string;
scheduled_at: string;
params: StatusParams;
media_attachments: Attachment[];
}
export interface StatusParams {
text: string;
in_reply_to_id: string;
media_ids: string[];
sensitive: boolean;
spoiler_text: string;
visibility: 'public' | 'unlisted' | 'private' | 'direct';
scheduled_at: string;
application_id: string;
}

View File

@ -72,9 +72,10 @@ export enum LeftPanelAction {
export enum LeftPanelType {
Closed = 0,
ManageAccount = 1,
CreateNewStatus = 2,
ManageAccount = 1,
CreateNewStatus = 2,
Search = 3,
AddNewAccount = 4,
Settings = 5
Settings = 5,
ScheduledStatuses = 6
}

View File

@ -1,8 +1,11 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
import { StatusWrapper } from '../models/common.model';
import { Account } from './models/mastodon.interfaces';
import { AccountInfo } from '../states/accounts.state';
import { ToolsService } from './tools.service';
@Injectable()
export class NotificationService {
@ -11,34 +14,52 @@ export class NotificationService {
public hideAccountUrlStream = new Subject<string>();
public deletedStatusStream = new Subject<StatusWrapper>();
constructor() {
constructor(private readonly toolsService: ToolsService) {
}
public notify(message: string, isError: boolean){
let newNotification = new NotificatioData(message, isError);
public notify(avatar: string, errorCode: number, message: string, isError: boolean) {
let newNotification = new NotificatioData(avatar, errorCode, message, isError);
this.notifactionStream.next(newNotification);
}
public notifyHttpError(err: HttpErrorResponse){
let message = 'Oops, Unknown Error' ;
try{
message = `Oops, Error ${err.status}`;
console.error(err.message);
} catch(err){}
this.notify(message, true);
public notifyHttpError(err: HttpErrorResponse, account: AccountInfo) {
let message = 'Oops, Unknown Error';
let code: number;
console.warn(err);
try {
code = err.status;
message = err.error.error; //Mastodon
if (!message) {
message = err.error.errors.detail; //Pleroma
}
} catch (err) { }
if (account) {
this.toolsService.getAvatar(account)
.then((avatar: string) => {
this.notify(avatar, code, message, true);
})
.catch(err => {
});
} else {
this.notify(null, code, message, true);
}
}
// public newStatusPosted(status: StatusWrapper){
public newStatusPosted(uiStatusRepliedToId: string, response: StatusWrapper){
public newStatusPosted(uiStatusRepliedToId: string, response: StatusWrapper) {
const notification = new NewReplyData(uiStatusRepliedToId, response);
this.newRespondPostedStream.next(notification);
}
public hideAccount(account: Account){
public hideAccount(account: Account) {
this.hideAccountUrlStream.next(account.url);
}
public deleteStatus(status: StatusWrapper){
public deleteStatus(status: StatusWrapper) {
this.deletedStatusStream.next(status);
}
}
@ -47,15 +68,17 @@ export class NotificatioData {
public id: string;
constructor(
public avatar: string,
public errorCode: number,
public message: string,
public isError: boolean
) {
) {
this.id = `${message}${new Date().getTime()}`;
}
}
export class NewReplyData {
constructor(public uiStatusId: string, public response: StatusWrapper){
constructor(public uiStatusId: string, public response: StatusWrapper) {
}
}

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ScheduledStatusService } from './scheduled-status.service';
xdescribe('ScheduledStatusService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ScheduledStatusService = TestBed.get(ScheduledStatusService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { BehaviorSubject } from 'rxjs';
import { MastodonService } from './mastodon.service';
import { AccountInfo } from '../states/accounts.state';
import { ScheduledStatus } from './models/mastodon.interfaces';
import { NotificationService } from './notification.service';
@Injectable({
providedIn: 'root'
})
export class ScheduledStatusService {
scheduledStatuses = new BehaviorSubject<ScheduledStatusNotification[]>([]);
constructor(
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly store: Store) {
this.fetchScheduledStatus();
}
private fetchScheduledStatus() {
let accounts = this.store.snapshot().registeredaccounts.accounts;
let promises: Promise<any>[] = [];
accounts.forEach((account: AccountInfo) => {
let promise = this.getStatusFromAccount(account);
promises.push(promise);
});
Promise.all(promises)
.then(() => {
setTimeout(() => {
this.fetchScheduledStatus();
}, 130 * 1000);
});
}
private getStatusFromAccount(account: AccountInfo): Promise<any> {
return this.mastodonService.getScheduledStatuses(account)
.then((statuses: ScheduledStatus[]) => {
if (statuses) {
this.processStatuses(account, statuses);
}
})
.catch(err => {
this.notificationService.notifyHttpError(err, account);
});
}
private processStatuses(account: AccountInfo, statuses: ScheduledStatus[]) {
let previousStatuses: ScheduledStatus[] = [];
const notification = this.scheduledStatuses.value.find(x => x.account.id === account.id);
if (notification) {
previousStatuses = notification.statuses;
}
let uniques: string[] = [];
if (statuses && previousStatuses) {
uniques = [...new Set([...statuses, ...previousStatuses].map(x => x.id))];
}
if (uniques.length !== previousStatuses.length) {
const currentStatuses = new ScheduledStatusNotification(account, statuses);
const otherNotifications = this.scheduledStatuses.value.filter(x => x.account.id !== account.id);
const currentNotifications = [...otherNotifications, currentStatuses];
this.scheduledStatuses.next(currentNotifications);
}
}
statusAdded(account: AccountInfo) {
this.getStatusFromAccount(account);
}
removeStatus(account: AccountInfo, statusId: string) {
const notification = this.scheduledStatuses.value.find(x => x.account.id === account.id);
notification.statuses = notification.statuses.filter(x => x.id !== statusId);
const otherNotifications = this.scheduledStatuses.value.filter(x => x.account.id !== account.id);
const currentNotifications = [...otherNotifications, notification];
this.scheduledStatuses.next(currentNotifications);
}
}
export class ScheduledStatusNotification {
constructor(
public readonly account: AccountInfo,
public statuses: ScheduledStatus[]) {
}
}

View File

@ -11,11 +11,28 @@ import { AccountSettings, SaveAccountSettings } from '../states/settings.state';
providedIn: 'root'
})
export class ToolsService {
private accountAvatar: { [id: string]: string; } = {};
constructor(
private readonly mastodonService: MastodonService,
private readonly store: Store) { }
getAvatar(acc: AccountInfo): Promise<string> {
if (this.accountAvatar[acc.id]) {
return Promise.resolve(this.accountAvatar[acc.id]);
} else {
return this.mastodonService.retrieveAccountDetails(acc)
.then((result: Account) => {
this.accountAvatar[acc.id] = result.avatar;
return result.avatar;
})
.catch((err) => {
return "";
});
}
}
getSelectedAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts.filter(x => x.isSelected);

View File

@ -40,7 +40,7 @@ export class UserNotificationService {
this.processNotifications(account, notifications);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
this.notificationService.notifyHttpError(err, account);
});
promises.push(getNotificationPromise);
});

View File

@ -80,4 +80,14 @@ $autosuggest-entry-color-hover: #e2e2e2;
$autosuggest-entry-handle-color-hover: #ffffff;
$scrollbar-color: #08090d;
$scrollbar-color-thumb: lighten($color-primary, 5);
$scrollbar-color-thumb: lighten($color-primary, 5);
$poll-editor-background: #3e455f;
$poll-editor-background: #32384d;
$poll-editor-background: #fff;
$poll-editor-separator: #e7e7e7;
$poll-editor-input-border: #b9b9b9;
$poll-editor-input-border-focus: #007be0;
$scheduler-background: #3e455f;

View File

@ -2,6 +2,7 @@
@import './mixins';
@import "~bootstrap/scss/bootstrap.scss";
@import "~ng-pick-datetime/assets/style/picker.min.css";
*,
*::after,