commit
c69ff3dd3a
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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; });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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[]) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -2,6 +2,7 @@
|
|||
@import './mixins';
|
||||
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
@import "~ng-pick-datetime/assets/style/picker.min.css";
|
||||
|
||||
*,
|
||||
*::after,
|
||||
|
|
Loading…
Reference in New Issue