extracting context-menu to its own component

This commit is contained in:
Nicolas Constant 2019-08-09 19:08:18 -04:00
parent 5d99d37384
commit b5f543dac3
No known key found for this signature in database
GPG Key ID: 1E9F677FB01A5688
8 changed files with 362 additions and 219 deletions

View File

@ -67,6 +67,7 @@ import { PollComponent } from './components/stream/status/poll/poll.component';
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';
const routes: Routes = [
@ -120,7 +121,8 @@ const routes: Routes = [
PollComponent,
TimeLeftPipe,
AutosuggestComponent,
EmojiPickerComponent
EmojiPickerComponent,
StatusUserContextMenuComponent
],
entryComponents: [
EmojiPickerComponent

View File

@ -28,7 +28,8 @@
<fa-icon [icon]="faWindowCloseRegular"></fa-icon>
</a>
<a href class="action-bar__link action-bar__link--more" (click)="onContextMenu($event)" title="More">
<app-status-user-context-menu class="action-bar__link action-bar__link--more" [statusWrapper]="statusWrapper" (browseThreadEvent)="browseThread($event)"></app-status-user-context-menu>
<!-- <a href class="action-bar__link action-bar__link--more" (click)="onContextMenu($event)" title="More">
<fa-icon [icon]="faEllipsisH"></fa-icon>
</a>
<context-menu #contextMenu>
@ -70,5 +71,5 @@
<ng-template contextMenuItem (execute)="delete(true)" *ngIf="isOwnerSelected">
Delete & re-draft
</ng-template>
</context-menu>
</context-menu> -->
</div>

View File

@ -1,5 +1,4 @@
@import "variables";
@import "context-menu";
.action-bar {
// outline: 1px solid greenyellow; // height: 20px;
@ -44,7 +43,7 @@
&--more {
position: absolute;
right: -5px;
right: 11px;
// left: 155px;
bottom: -2px;
}
@ -150,5 +149,4 @@
-moz-animation: loadingAnimation 1s infinite;
-o-animation: loadingAnimation 1s infinite;
animation: loadingAnimation 1s infinite;
}

View File

@ -1,10 +1,9 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Store } from '@ngxs/store';
import { Observable, Subscription } from 'rxjs';
import { faWindowClose, faReply, faRetweet, faStar, faEllipsisH, faLock, faEnvelope } from "@fortawesome/free-solid-svg-icons";
import { faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons";
import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
import { MastodonService } from '../../../../services/mastodon.service';
import { AccountInfo } from '../../../../states/accounts.state';
@ -12,7 +11,6 @@ import { Status, Account, Results } from '../../../../services/models/mastodon.i
import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service';
import { NotificationService } from '../../../../services/notification.service';
import { StatusWrapper } from '../../../../models/common.model';
import { NavigationService } from '../../../../services/navigation.service';
@Component({
selector: 'app-action-bar',
@ -29,12 +27,6 @@ export class ActionBarComponent implements OnInit, OnDestroy {
faLock = faLock;
faEnvelope = faEnvelope;
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
public items = [
{ name: 'John', otherProperty: 'Foo' },
{ name: 'Joe', otherProperty: 'Bar' }
];
@Input() statusWrapper: StatusWrapper;
@Output() replyEvent = new EventEmitter();
@Output() cwIsActiveEvent = new EventEmitter<boolean>();
@ -51,27 +43,20 @@ export class ActionBarComponent implements OnInit, OnDestroy {
favoriteIsLoading: boolean;
boostIsLoading: boolean;
isContentWarningActive: boolean = false;
isContentWarningActive: boolean = false;
isOwnerSelected: boolean;
displayedStatus: Status;
private isProviderSelected: boolean;
private selectedAccounts: AccountInfo[];
username: string;
displayedStatus: Status;
private fullHandle: string;
private loadedAccounts: AccountInfo[];
private favoriteStatePerAccountId: { [id: string]: boolean; } = {};
private bootedStatePerAccountId: { [id: string]: boolean; } = {};
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
constructor(
private readonly navigationService: NavigationService,
private readonly contextMenuService: ContextMenuService,
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService,
@ -87,12 +72,10 @@ export class ActionBarComponent implements OnInit, OnDestroy {
if (status.reblog) {
this.favoriteStatePerAccountId[account.id] = status.reblog.favourited;
this.bootedStatePerAccountId[account.id] = status.reblog.reblogged;
this.extractHandle(status.reblog.account);
this.displayedStatus = status.reblog;
} else {
this.favoriteStatePerAccountId[account.id] = status.favourited;
this.bootedStatePerAccountId[account.id] = status.reblogged;
this.extractHandle(status.account);
this.displayedStatus = status;
}
@ -101,18 +84,10 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.loadedAccounts = accounts;
this.checkStatus(accounts);
});
}
private extractHandle(account: Account) {
this.username = account.acct.split('@')[0];
this.fullHandle = this.toolsService.getAccountFullHandle(account);
// this.fullHandle = `@${this.fullHandle}`;
}
ngOnDestroy(): void {
this.accountSub.unsubscribe();
}
@ -123,9 +98,6 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.selectedAccounts = accounts.filter(x => x.isSelected);
this.isProviderSelected = this.selectedAccounts.filter(x => x.id === provider.id).length > 0;
this.isOwnerSelected = this.selectedAccounts[0].username === this.displayedStatus.account.username
&& this.selectedAccounts[0].instance === this.displayedStatus.account.url.replace('https://', '').split('/')[0];
if (status.visibility === 'direct' || status.visibility === 'private') {
this.isBoostLocked = true;
} else {
@ -246,187 +218,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
} else {
this.isFavorited = false;
}
}
}
public onContextMenu($event: MouseEvent): void {
this.contextMenuService.show.next({
// Optional - if unspecified, all context menu components will open
contextMenu: this.contextMenu,
event: $event,
item: null
});
$event.preventDefault();
$event.stopPropagation();
}
expandStatus(): boolean {
const openThread = new OpenThreadEvent(this.displayedStatus, this.statusWrapper.provider);
this.browseThreadEvent.next(openThread);
return false;
}
copyStatusLink(): boolean {
let selBox = document.createElement('textarea');
selBox.style.position = 'fixed';
selBox.style.left = '0';
selBox.style.top = '0';
selBox.style.opacity = '0';
selBox.value = this.displayedStatus.url;
document.body.appendChild(selBox);
selBox.focus();
selBox.select();
document.execCommand('copy');
document.body.removeChild(selBox);
return false;
}
mentionAccount(): boolean {
this.navigationService.replyToUser(this.fullHandle, false);
return false;
}
dmAccount(): boolean {
this.navigationService.replyToUser(this.fullHandle, true);
return false;
}
muteAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.mute(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
});
return false;
}
blockAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.block(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
});
return false;
}
muteConversation(): boolean {
const selectedAccount = this.selectedAccounts[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.muteConversation(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
unmuteConversation(): boolean {
const selectedAccount = this.selectedAccounts[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.unmuteConversation(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
pinOnProfile(): boolean {
const selectedAccount = this.selectedAccounts[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.pinOnProfile(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
unpinFromProfile(): boolean {
const selectedAccount = this.selectedAccounts[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.unpinFromProfile(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
delete(redraft: boolean): boolean {
const selectedAccount = this.selectedAccounts[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.deleteStatus(selectedAccount, status.id);
})
.then(() => {
if (redraft) {
this.navigationService.redraft(this.statusWrapper)
}
const deletedStatus = new StatusWrapper(this.displayedStatus, selectedAccount);
this.notificationService.deleteStatus(deletedStatus);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
private getStatus(account: AccountInfo): Promise<Status> {
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (account.id !== this.statusWrapper.provider.id) {
statusPromise = this.mastodonService.search(account, this.statusWrapper.status.url, true)
.then((result: Results) => {
return result.statuses[0];
});
}
return statusPromise;
browseThread(event: OpenThreadEvent){
this.browseThreadEvent.next(event);
}
}

View File

@ -0,0 +1,45 @@
<a href class="context-menu-link" (click)="onContextMenu($event)"
[class.context-menu-link__status]="statusWrapper"
title="More">
<fa-icon [icon]="faEllipsisH"></fa-icon>
</a>
<context-menu #contextMenu>
<ng-template contextMenuItem (execute)="expandStatus()">
Expand status
</ng-template>
<ng-template contextMenuItem (execute)="copyStatusLink()">
Copy link to status
</ng-template>
<ng-template contextMenuItem divider="true"></ng-template>
<ng-template contextMenuItem (execute)="mentionAccount()" *ngIf="!isOwnerSelected">
Mention @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="dmAccount()" *ngIf="!isOwnerSelected">
Direct message @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="muteConversation()" *ngIf="isOwnerSelected && !displayedStatus.muted">
Mute conversation
</ng-template>
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="isOwnerSelected && displayedStatus.muted">
Unmute conversation
</ng-template>
<ng-template contextMenuItem divider="true"></ng-template>
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected">
Mute @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="blockAccount()" *ngIf="!isOwnerSelected">
Block @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
Pin on profile
</ng-template>
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
Unpin from profile
</ng-template>
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="isOwnerSelected">
Delete
</ng-template>
<ng-template contextMenuItem (execute)="delete(true)" *ngIf="isOwnerSelected">
Delete & re-draft
</ng-template>
</context-menu>

View File

@ -0,0 +1,16 @@
@import "variables";
@import "context-menu";
.context-menu-link {
&__status {
color: $status-secondary-color;
&:hover {
color: $status-links-color;
}
}
&__profile {
}
}

View File

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

View File

@ -0,0 +1,262 @@
import { Component, OnInit, ViewChild, Output, EventEmitter, Input, OnDestroy } from '@angular/core';
import { faEllipsisH } from "@fortawesome/free-solid-svg-icons";
import { ContextMenuComponent, ContextMenuService } from 'ngx-contextmenu';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { Status, Account, Results } from '../../../../../services/models/mastodon.interfaces';
import { ToolsService, OpenThreadEvent } from '../../../../../services/tools.service';
import { StatusWrapper } from '../../../../../models/common.model';
import { NavigationService } from '../../../../../services/navigation.service';
import { AccountInfo } from '../../../../../states/accounts.state';
import { MastodonService } from '../../../../../services/mastodon.service';
import { NotificationService } from '../../../../../services/notification.service';
@Component({
selector: 'app-status-user-context-menu',
templateUrl: './status-user-context-menu.component.html',
styleUrls: ['./status-user-context-menu.component.scss']
})
export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
faEllipsisH = faEllipsisH;
private fullHandle: string;
private loadedAccounts: AccountInfo[];
displayedStatus: Status;
username: string;
isOwnerSelected: boolean;
@Input() statusWrapper: StatusWrapper;
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService,
private readonly notificationService: NotificationService,
private readonly navigationService: NavigationService,
private readonly toolsService: ToolsService,
private readonly contextMenuService: ContextMenuService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
const status = this.statusWrapper.status;
if (status.reblog) {
this.displayedStatus = status.reblog;
} else {
this.displayedStatus = status;
}
this.username = this.displayedStatus.account.acct.split('@')[0];
this.fullHandle = this.toolsService.getAccountFullHandle(this.displayedStatus.account);
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.loadedAccounts = accounts;
this.checkStatus(accounts);
});
}
private checkStatus(accounts: AccountInfo[]): void {
const selectedAccount = accounts.find(x => x.isSelected);
this.isOwnerSelected = selectedAccount.username === this.displayedStatus.account.username
&& selectedAccount.instance === this.displayedStatus.account.url.replace('https://', '').split('/')[0];
}
ngOnDestroy(): void {
this.accountSub.unsubscribe();
}
public onContextMenu($event: MouseEvent): void {
this.contextMenuService.show.next({
// Optional - if unspecified, all context menu components will open
contextMenu: this.contextMenu,
event: $event,
item: null
});
$event.preventDefault();
$event.stopPropagation();
}
expandStatus(): boolean {
const openThread = new OpenThreadEvent(this.displayedStatus, this.statusWrapper.provider);
this.browseThreadEvent.next(openThread);
return false;
}
copyStatusLink(): boolean {
let selBox = document.createElement('textarea');
selBox.style.position = 'fixed';
selBox.style.left = '0';
selBox.style.top = '0';
selBox.style.opacity = '0';
selBox.value = this.displayedStatus.url;
document.body.appendChild(selBox);
selBox.focus();
selBox.select();
document.execCommand('copy');
document.body.removeChild(selBox);
return false;
}
mentionAccount(): boolean {
this.navigationService.replyToUser(this.fullHandle, false);
return false;
}
dmAccount(): boolean {
this.navigationService.replyToUser(this.fullHandle, true);
return false;
}
muteAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.mute(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
});
return false;
}
blockAccount(): boolean {
this.loadedAccounts.forEach(acc => {
this.toolsService.findAccount(acc, this.fullHandle)
.then((target: Account) => {
this.mastodonService.block(acc, target.id);
return target;
})
.then((target: Account) => {
this.notificationService.hideAccount(target);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
});
return false;
}
muteConversation(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.muteConversation(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
unmuteConversation(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.unmuteConversation(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.muted = status.muted;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
pinOnProfile(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
console.warn(status);
return this.mastodonService.pinOnProfile(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
unpinFromProfile(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.unpinFromProfile(selectedAccount, status.id)
})
.then((status: Status) => {
this.displayedStatus.pinned = status.pinned;
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
delete(redraft: boolean): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
this.getStatus(selectedAccount)
.then((status: Status) => {
return this.mastodonService.deleteStatus(selectedAccount, status.id);
})
.then(() => {
if (redraft) {
this.navigationService.redraft(this.statusWrapper)
}
const deletedStatus = new StatusWrapper(this.displayedStatus, selectedAccount);
this.notificationService.deleteStatus(deletedStatus);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
return false;
}
private getStatus(account: AccountInfo): Promise<Status> {
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (account.id !== this.statusWrapper.provider.id) {
statusPromise = this.mastodonService.search(account, this.statusWrapper.status.url, true)
.then((result: Results) => {
return result.statuses[0];
});
}
return statusPromise;
}
}