commit
f126cf2b37
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sengi",
|
"name": "sengi",
|
||||||
"version": "0.9.1",
|
"version": "0.10.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "main-electron.js",
|
"main": "main-electron.js",
|
||||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||||
|
|
|
@ -60,6 +60,8 @@ import { AccountEmojiPipe } from './pipes/account-emoji.pipe';
|
||||||
import { CardComponent } from './components/stream/status/card/card.component';
|
import { CardComponent } from './components/stream/status/card/card.component';
|
||||||
import { ListEditorComponent } from './components/floating-column/manage-account/my-account/list-editor/list-editor.component';
|
import { ListEditorComponent } from './components/floating-column/manage-account/my-account/list-editor/list-editor.component';
|
||||||
import { ListAccountComponent } from './components/floating-column/manage-account/my-account/list-editor/list-account/list-account.component';
|
import { ListAccountComponent } from './components/floating-column/manage-account/my-account/list-editor/list-account/list-account.component';
|
||||||
|
import { PollComponent } from './components/stream/status/poll/poll.component';
|
||||||
|
import { TimeLeftPipe } from './pipes/time-left.pipe';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||||
|
@ -108,7 +110,9 @@ const routes: Routes = [
|
||||||
AccountEmojiPipe,
|
AccountEmojiPipe,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
ListEditorComponent,
|
ListEditorComponent,
|
||||||
ListAccountComponent
|
ListAccountComponent,
|
||||||
|
PollComponent,
|
||||||
|
TimeLeftPipe
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
|
|
|
@ -154,6 +154,7 @@ class NotificationWrapper {
|
||||||
case 'mention':
|
case 'mention':
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
|
case 'poll':
|
||||||
this.status= new StatusWrapper(notification.status, provider);
|
this.status= new StatusWrapper(notification.status, provider);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -164,5 +165,5 @@ class NotificationWrapper {
|
||||||
wrapperId: string;
|
wrapperId: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
status: StatusWrapper;
|
status: StatusWrapper;
|
||||||
type: 'mention' | 'reblog' | 'favourite' | 'follow';
|
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
|
||||||
}
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="poll">
|
||||||
|
<div *ngIf="!poll.voted && !poll.expired">
|
||||||
|
<div *ngFor="let o of options">
|
||||||
|
<label class="poll__container">{{o.title}}
|
||||||
|
<input class="poll__container__input" type="{{choiceType}}" name="{{pollName}}" value="{{o.title}}"
|
||||||
|
(change)="onSelectionChange(o)">
|
||||||
|
<span class="poll__container__checkmark" *ngIf="!pollLocked"
|
||||||
|
[ngClass]="{'poll__container__checkmark--box' : choiceType=='checkbox', 'poll__container__checkmark--round': choiceType=='radio'}"></span>
|
||||||
|
<fa-icon *ngIf="pollLocked" class="poll__container__lock" title="Account can't access this poll" [icon]="faLock"></fa-icon>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="poll.voted || poll.expired">
|
||||||
|
<div *ngFor="let o of options">
|
||||||
|
<div class="poll__result" title="{{ o.votes_count }} votes">
|
||||||
|
<div class="poll__result--progress-bar" [style.width]="o.percentage + '%'" [ngClass]="{ 'poll__result--progress-bar--most-votes': o.isMax }"></div>
|
||||||
|
<div class="poll__result--data"> <span class="poll__result--percentage">{{ o.percentage }}%</span>
|
||||||
|
{{o.title}}</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="poll__voting">
|
||||||
|
<button href *ngIf="!poll.voted && !poll.expired && !pollLocked" class="btn btn-sm btn-custom-primary poll__btn-vote"
|
||||||
|
title="don't boo, vote!" (click)="vote()">Vote</button>
|
||||||
|
<a href class="poll__refresh" *ngIf="(poll.voted || poll.expired) && !pollLocked" title="refresh poll" (click)="refresh()">refresh</a>
|
||||||
|
<div class="poll__statistics"><span *ngIf="(poll.voted || poll.expired) && !pollLocked" class="poll__separator">·</span>{{poll.votes_count}} votes<span *ngIf="!poll.expired" class="poll__separator" title="{{ poll.expires_at | timeLeft | async }}">· {{ poll.expires_at | timeLeft | async }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,184 @@
|
||||||
|
// @import "variables";
|
||||||
|
// @import "commons";
|
||||||
|
// @import "panel";
|
||||||
|
@import "buttons";
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
color: rgb(228, 228, 228);
|
||||||
|
|
||||||
|
&__btn-vote {
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
padding: 4px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__statistics {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: rgb(101, 121, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__voting {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__refresh {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: rgb(101, 121, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
margin: 0 0 5px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
// font-size: 22px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover &__input~&__checkmark {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
& &__input:checked~&__checkmark {
|
||||||
|
background-color: rgb(62, 75, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lock {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkmark--box {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: #eee;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkmark--round {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& &__input:checked:checked~&__checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& &__checkmark--box:after {
|
||||||
|
left: 7px;
|
||||||
|
top: 5px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 3px 3px 0;
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkmark--round:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& &__checkmark--round:after {
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__result {
|
||||||
|
transition: width 2s;
|
||||||
|
|
||||||
|
margin: 0 0 5px 5px;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
position: relative;
|
||||||
|
height: 27px;
|
||||||
|
|
||||||
|
&--data {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--percentage {
|
||||||
|
color: rgb(228, 228, 228);
|
||||||
|
color: white;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&--progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgb(47, 68, 100);
|
||||||
|
// background-color: rgb(43, 62, 92);
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
width: calc(100%);
|
||||||
|
height: 22px;
|
||||||
|
z-index: 1;
|
||||||
|
float: left;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&--most-votes {
|
||||||
|
background-color: rgb(18, 118, 158);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__separator {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
/* iOS Safari */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
/* Safari */
|
||||||
|
-khtml-user-select: none;
|
||||||
|
/* Konqueror HTML */
|
||||||
|
-moz-user-select: none;
|
||||||
|
/* Firefox */
|
||||||
|
-ms-user-select: none;
|
||||||
|
/* Internet Explorer/Edge */
|
||||||
|
user-select: none;
|
||||||
|
/* Non-prefixed version, currently supported by Chrome and Opera */
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PollComponent } from './poll.component';
|
||||||
|
|
||||||
|
xdescribe('PollComponent', () => {
|
||||||
|
let component: PollComponent;
|
||||||
|
let fixture: ComponentFixture<PollComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ PollComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PollComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
|
import { Store } from '@ngxs/store';
|
||||||
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Poll, PollOption, Status } from '../../../../services/models/mastodon.interfaces';
|
||||||
|
import { AccountInfo } from '../../../../states/accounts.state';
|
||||||
|
import { MastodonService } from '../../../../services/mastodon.service';
|
||||||
|
import { NotificationService } from '../../../../services/notification.service';
|
||||||
|
import { ToolsService } from '../../../../services/tools.service';
|
||||||
|
import { StatusWrapper } from '../../../../models/common.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-poll',
|
||||||
|
templateUrl: './poll.component.html',
|
||||||
|
styleUrls: ['./poll.component.scss']
|
||||||
|
})
|
||||||
|
export class PollComponent implements OnInit {
|
||||||
|
faLock = faLock;
|
||||||
|
|
||||||
|
pollName: string;
|
||||||
|
choiceType: string;
|
||||||
|
pollLocked: boolean;
|
||||||
|
|
||||||
|
private pollSelection: number[] = [];
|
||||||
|
options: PollOptionWrapper[] = [];
|
||||||
|
|
||||||
|
private pollPerAccountId: { [id: string]: Promise<Poll>; } = {};
|
||||||
|
|
||||||
|
private _poll: Poll;
|
||||||
|
@Input('poll')
|
||||||
|
set poll(value: Poll) {
|
||||||
|
this._poll = value;
|
||||||
|
|
||||||
|
this.pollName = this.poll.id;
|
||||||
|
|
||||||
|
if (this.poll.multiple) {
|
||||||
|
this.choiceType = 'checkbox';
|
||||||
|
} else {
|
||||||
|
this.choiceType = 'radio';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.length = 0;
|
||||||
|
const maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
|
||||||
|
let i = 0;
|
||||||
|
for (let opt of this.poll.options) {
|
||||||
|
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, opt.votes_count === maxVotes);
|
||||||
|
this.options.push(optWrapper);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get poll(): Poll {
|
||||||
|
return this._poll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() provider: AccountInfo;
|
||||||
|
@Input() status: Status;
|
||||||
|
|
||||||
|
private accounts$: Observable<AccountInfo[]>;
|
||||||
|
private accountSub: Subscription;
|
||||||
|
|
||||||
|
private selectedAccount: AccountInfo;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly store: Store,
|
||||||
|
private notificationService: NotificationService,
|
||||||
|
private toolsService: ToolsService,
|
||||||
|
private mastodonService: MastodonService) {
|
||||||
|
|
||||||
|
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.pollPerAccountId[this.provider.id] = Promise.resolve(this.poll);
|
||||||
|
this.selectedAccount = this.provider;
|
||||||
|
|
||||||
|
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||||
|
this.checkStatus(accounts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStatus(accounts: AccountInfo[]): void {
|
||||||
|
this.pollLocked = false;
|
||||||
|
var newSelectedAccount = accounts.find(x => x.isSelected);
|
||||||
|
|
||||||
|
const accountChanged = this.selectedAccount.id !== newSelectedAccount.id;
|
||||||
|
if (accountChanged && !this.pollPerAccountId[newSelectedAccount.id] && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')) {
|
||||||
|
this.setStatsAtZero();
|
||||||
|
|
||||||
|
this.pollPerAccountId[newSelectedAccount.id] = this.toolsService.getStatusUsableByAccount(newSelectedAccount, new StatusWrapper(this.status, this.provider))
|
||||||
|
.then((status: Status) => {
|
||||||
|
return this.mastodonService.getPoll(newSelectedAccount, status.poll.id);
|
||||||
|
})
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
this.poll = poll;
|
||||||
|
return poll;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.notificationService.notifyHttpError(err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} else if (this.status.visibility !== 'public' && this.status.visibility !== 'unlisted' && this.provider.id !== newSelectedAccount.id) {
|
||||||
|
this.pollLocked = true;
|
||||||
|
} else {
|
||||||
|
this.pollPerAccountId[newSelectedAccount.id]
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
this.poll = poll;
|
||||||
|
})
|
||||||
|
.catch(err => this.notificationService.notifyHttpError(err));
|
||||||
|
}
|
||||||
|
this.selectedAccount = newSelectedAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vote(): boolean {
|
||||||
|
const selectedAccount = this.selectedAccount;
|
||||||
|
const pollPromise = this.pollPerAccountId[selectedAccount.id];
|
||||||
|
|
||||||
|
pollPromise
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
return this.mastodonService.voteOnPoll(selectedAccount, poll.id, this.pollSelection);
|
||||||
|
})
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
this.poll = poll;
|
||||||
|
this.pollPerAccountId[selectedAccount.id] = Promise.resolve(poll);
|
||||||
|
})
|
||||||
|
.catch(err => this.notificationService.notifyHttpError(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setStatsAtZero() {
|
||||||
|
this.options.forEach(p => {
|
||||||
|
p.votes_count = 0;
|
||||||
|
p.percentage = '0';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(): boolean {
|
||||||
|
this.setStatsAtZero();
|
||||||
|
|
||||||
|
const selectedAccount = this.selectedAccount;
|
||||||
|
const pollPromise = this.pollPerAccountId[selectedAccount.id];
|
||||||
|
|
||||||
|
pollPromise
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
return this.mastodonService.getPoll(selectedAccount, poll.id);
|
||||||
|
})
|
||||||
|
.then((poll: Poll) => {
|
||||||
|
this.poll = poll;
|
||||||
|
this.pollPerAccountId[selectedAccount.id] = Promise.resolve(poll);
|
||||||
|
})
|
||||||
|
.catch(err => this.notificationService.notifyHttpError(err));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionChange(entry: PollOptionWrapper) {
|
||||||
|
let index = entry.id;
|
||||||
|
if (this.poll.multiple) {
|
||||||
|
if (this.pollSelection.includes(index)) {
|
||||||
|
this.pollSelection = this.pollSelection.filter(x => x !== index);
|
||||||
|
} else {
|
||||||
|
this.pollSelection.push(index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.pollSelection.length = 0;
|
||||||
|
this.pollSelection.push(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollOptionWrapper implements PollOption {
|
||||||
|
constructor(index: number, option: PollOption, totalVotes: number, isMax: boolean) {
|
||||||
|
this.id = index;
|
||||||
|
this.title = option.title;
|
||||||
|
this.votes_count = option.votes_count;
|
||||||
|
if (totalVotes === 0) {
|
||||||
|
this.percentage = '0';
|
||||||
|
} else {
|
||||||
|
this.percentage = ((this.votes_count / totalVotes) * 100).toFixed(0);
|
||||||
|
}
|
||||||
|
this.isMax = isMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
votes_count: number;
|
||||||
|
percentage: string;
|
||||||
|
isMax: boolean;
|
||||||
|
}
|
|
@ -21,6 +21,14 @@
|
||||||
innerHTML="{{ notificationAccount | accountEmoji }}"></a> boosted your status
|
innerHTML="{{ notificationAccount | accountEmoji }}"></a> boosted your status
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="notificationType === 'poll'">
|
||||||
|
<div class="notification--icon">
|
||||||
|
<fa-icon class="boost" [icon]="faList"></fa-icon>
|
||||||
|
</div>
|
||||||
|
<div class="notification--label">
|
||||||
|
A poll you have voted in has ended
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div [ngClass]="{'notification--status': notificationAccount }">
|
<div [ngClass]="{'notification--status': notificationAccount }">
|
||||||
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
|
||||||
|
@ -67,6 +75,9 @@
|
||||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||||
(textSelected)="textSelected()"></app-databinded-text>
|
(textSelected)="textSelected()"></app-databinded-text>
|
||||||
|
|
||||||
|
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
|
||||||
|
[poll]="displayedStatus.poll" [status]="displayedStatus" [provider]="statusWrapper.provider"></app-poll>
|
||||||
|
|
||||||
<app-card class="status__card" *ngIf="!isContentWarned && displayedStatus.card && !hasAttachments" [card]="displayedStatus.card"></app-card>
|
<app-card class="status__card" *ngIf="!isContentWarned && displayedStatus.card && !hasAttachments" [card]="displayedStatus.card"></app-card>
|
||||||
|
|
||||||
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
|
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
|
||||||
|
|
|
@ -116,6 +116,11 @@
|
||||||
margin: 10px 10px 0 $avatar-column-space;
|
margin: 10px 10px 0 $avatar-column-space;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
&__poll {
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 10px 10px 0 $avatar-column-space;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
&__content-warning {
|
&__content-warning {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
display: block; // border: 1px solid greenyellow;
|
display: block; // border: 1px solid greenyellow;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from "@angular/core";
|
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from "@angular/core";
|
||||||
import { faStar, faRetweet } from "@fortawesome/free-solid-svg-icons";
|
import { faStar, faRetweet, faList } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { Status, Account } from "../../../services/models/mastodon.interfaces";
|
import { Status, Account } from "../../../services/models/mastodon.interfaces";
|
||||||
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
|
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
|
||||||
|
@ -17,6 +17,7 @@ export class StatusComponent implements OnInit {
|
||||||
|
|
||||||
faStar = faStar;
|
faStar = faStar;
|
||||||
faRetweet = faRetweet;
|
faRetweet = faRetweet;
|
||||||
|
faList = faList;
|
||||||
|
|
||||||
displayedStatus: Status;
|
displayedStatus: Status;
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ export class StatusComponent implements OnInit {
|
||||||
|
|
||||||
@Input() isThreadDisplay: boolean;
|
@Input() isThreadDisplay: boolean;
|
||||||
|
|
||||||
@Input() notificationType: 'mention' | 'reblog' | 'favourite';
|
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll';
|
||||||
@Input() notificationAccount: Account;
|
@Input() notificationAccount: Account;
|
||||||
|
|
||||||
private _statusWrapper: StatusWrapper;
|
private _statusWrapper: StatusWrapper;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { TimeLeftPipe } from './time-left.pipe';
|
||||||
|
|
||||||
|
xdescribe('TimeLeftPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new TimeLeftPipe(null);
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { Pipe, PipeTransform, NgZone } from '@angular/core';
|
||||||
|
import { Observable, Observer } from 'rxjs';
|
||||||
|
|
||||||
|
interface processOutput {
|
||||||
|
text: string; // Convert timestamp to string
|
||||||
|
timeToUpdate: number; // Time until update in milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'timeLeft'
|
||||||
|
})
|
||||||
|
export class TimeLeftPipe implements PipeTransform {
|
||||||
|
|
||||||
|
constructor(private ngZone: NgZone) { }
|
||||||
|
|
||||||
|
private process = (timestamp: number): processOutput => {
|
||||||
|
let text: string;
|
||||||
|
let timeToUpdate: number;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Time ago in milliseconds
|
||||||
|
const timeLeft: number = timestamp - now.getTime();
|
||||||
|
|
||||||
|
const seconds = timeLeft / 1000;
|
||||||
|
const minutes = seconds / 60;
|
||||||
|
const hours = minutes / 60;
|
||||||
|
const days = hours / 24;
|
||||||
|
// const months = days / 30.416;
|
||||||
|
// const years = days / 365;
|
||||||
|
|
||||||
|
if (seconds < 0) {
|
||||||
|
text = '0 seconds left';
|
||||||
|
} else if (seconds <= 60) {
|
||||||
|
text = Math.round(seconds) + ' seconds left';
|
||||||
|
} else if (minutes <= 90) {
|
||||||
|
text = Math.round(minutes) + ' minutes left';
|
||||||
|
} else if (hours <= 24) {
|
||||||
|
text = Math.round(hours) + ' hours left';
|
||||||
|
} else {
|
||||||
|
text = Math.round(days) + ' days left';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds < -10) {
|
||||||
|
// update every week
|
||||||
|
timeToUpdate = 7 * 24 * 3600 * 1000;
|
||||||
|
} else if (minutes < 1) {
|
||||||
|
// update every 2 secs
|
||||||
|
timeToUpdate = 2 * 1000;
|
||||||
|
} else if (hours < 1) {
|
||||||
|
// update every 30 secs
|
||||||
|
timeToUpdate = 30 * 1000;
|
||||||
|
} else if (days < 1) {
|
||||||
|
// update every 5 mins
|
||||||
|
timeToUpdate = 300 * 1000;
|
||||||
|
} else {
|
||||||
|
// update every hour
|
||||||
|
timeToUpdate = 3600 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
timeToUpdate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public transform = (value: string | Date): Observable<string> => {
|
||||||
|
let d: Date;
|
||||||
|
if (value instanceof Date) {
|
||||||
|
d = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d = new Date(value);
|
||||||
|
}
|
||||||
|
// time value in milliseconds
|
||||||
|
const timestamp = d.getTime();
|
||||||
|
|
||||||
|
let timeoutID: any;
|
||||||
|
|
||||||
|
return Observable.create((observer: Observer<string>) => {
|
||||||
|
let latestText = '';
|
||||||
|
|
||||||
|
// Repeatedly set new timeouts for new update checks.
|
||||||
|
const registerUpdate = () => {
|
||||||
|
const processOutput = this.process(timestamp);
|
||||||
|
if (processOutput.text !== latestText) {
|
||||||
|
latestText = processOutput.text;
|
||||||
|
this.ngZone.run(() => {
|
||||||
|
observer.next(latestText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
timeoutID = setTimeout(registerUpdate, processOutput.timeToUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ngZone.runOutsideAngular(registerUpdate);
|
||||||
|
|
||||||
|
// Return teardown function
|
||||||
|
const teardownFunction = () => {
|
||||||
|
if (timeoutID) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return teardownFunction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
|
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
import { ApiRoutes } from './models/api.settings';
|
import { ApiRoutes } from './models/api.settings';
|
||||||
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List } from "./models/mastodon.interfaces";
|
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll } from "./models/mastodon.interfaces";
|
||||||
import { AccountInfo } from '../states/accounts.state';
|
import { AccountInfo } from '../states/accounts.state';
|
||||||
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MastodonService {
|
export class MastodonService {
|
||||||
private apiRoutes = new ApiRoutes();
|
private apiRoutes = new ApiRoutes();
|
||||||
|
|
||||||
constructor(private readonly httpClient: HttpClient) { }
|
constructor(private readonly httpClient: HttpClient) { }
|
||||||
|
@ -306,6 +306,20 @@ export class MastodonService {
|
||||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
return this.httpClient.delete(route, { headers: headers }).toPromise();
|
return this.httpClient.delete(route, { headers: headers }).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
voteOnPoll(account: AccountInfo, pollId: string, pollSelection: number[]): Promise<Poll> {
|
||||||
|
let route = `https://${account.instance}${this.apiRoutes.voteOnPoll}`.replace('{0}', pollId);
|
||||||
|
route += `?${this.formatArray(pollSelection.map(x => x.toString()), 'choices')}`;
|
||||||
|
|
||||||
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
|
return this.httpClient.post<Poll>(route, null, { headers: headers }).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPoll(account: AccountInfo, pollId: string): Promise<Poll> {
|
||||||
|
let route = `https://${account.instance}${this.apiRoutes.getPoll}`.replace('{0}', pollId);
|
||||||
|
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||||
|
return this.httpClient.get<Poll>(route, { headers: headers }).toPromise();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum VisibilityEnum {
|
export enum VisibilityEnum {
|
||||||
|
|
|
@ -59,4 +59,6 @@ export class ApiRoutes {
|
||||||
deleteList = '/api/v1/lists/{0}';
|
deleteList = '/api/v1/lists/{0}';
|
||||||
addAccountToList = '/api/v1/lists/{0}/accounts';
|
addAccountToList = '/api/v1/lists/{0}/accounts';
|
||||||
removeAccountFromList = '/api/v1/lists/{0}/accounts';
|
removeAccountFromList = '/api/v1/lists/{0}/accounts';
|
||||||
|
voteOnPoll = '/api/v1/polls/{0}/votes';
|
||||||
|
getPoll = '/api/v1/polls/{0}';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { PlatformLocation } from '@angular/common';
|
||||||
|
|
||||||
export interface AppData {
|
export interface AppData {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_secret: string;
|
client_secret: string;
|
||||||
|
@ -122,7 +124,7 @@ export interface Mention {
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'mention' | 'reblog' | 'favourite' | 'follow';
|
type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
status?: Status;
|
status?: Status;
|
||||||
|
@ -174,6 +176,7 @@ export interface Status {
|
||||||
language: string;
|
language: string;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
card: Card;
|
card: Card;
|
||||||
|
poll: Poll;
|
||||||
|
|
||||||
pleroma: PleromaStatusInfo;
|
pleroma: PleromaStatusInfo;
|
||||||
}
|
}
|
||||||
|
@ -191,4 +194,19 @@ export interface Tag {
|
||||||
export interface List {
|
export interface List {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Poll {
|
||||||
|
id: string;
|
||||||
|
expires_at: string;
|
||||||
|
expired: boolean;
|
||||||
|
multiple: boolean;
|
||||||
|
votes_count: number;
|
||||||
|
options: PollOption[];
|
||||||
|
voted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollOption {
|
||||||
|
title: string;
|
||||||
|
votes_count: number;
|
||||||
}
|
}
|
Loading…
Reference in New Issue