diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b7a78fbc..27e7c5e3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 diff --git a/src/app/components/stream/status/action-bar/action-bar.component.html b/src/app/components/stream/status/action-bar/action-bar.component.html index 0720e8f7..f783c925 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.html +++ b/src/app/components/stream/status/action-bar/action-bar.component.html @@ -28,7 +28,8 @@ - + + \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/action-bar.component.scss b/src/app/components/stream/status/action-bar/action-bar.component.scss index e109df3e..e49a119d 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.scss +++ b/src/app/components/stream/status/action-bar/action-bar.component.scss @@ -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; - } \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/action-bar.component.ts b/src/app/components/stream/status/action-bar/action-bar.component.ts index 12fe5de7..31a5c17c 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.ts +++ b/src/app/components/stream/status/action-bar/action-bar.component.ts @@ -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(); @@ -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; 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 { - let statusPromise: Promise = 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); } } diff --git a/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.html b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.html new file mode 100644 index 00000000..c681df4b --- /dev/null +++ b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.html @@ -0,0 +1,45 @@ + + + + + + Expand status + + + Copy link to status + + + + Mention @{{ this.username }} + + + Direct message @{{ this.username }} + + + Mute conversation + + + Unmute conversation + + + + Mute @{{ this.username }} + + + Block @{{ this.username }} + + + Pin on profile + + + Unpin from profile + + + Delete + + + Delete & re-draft + + \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.scss b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.scss new file mode 100644 index 00000000..330bff85 --- /dev/null +++ b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.scss @@ -0,0 +1,16 @@ +@import "variables"; +@import "context-menu"; + +.context-menu-link { + &__status { + color: $status-secondary-color; + + &:hover { + color: $status-links-color; + } + } + + &__profile { + + } +} \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.spec.ts b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.spec.ts new file mode 100644 index 00000000..833bea46 --- /dev/null +++ b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ StatusUserContextMenuComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StatusUserContextMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.ts b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.ts new file mode 100644 index 00000000..f90cacd5 --- /dev/null +++ b/src/app/components/stream/status/action-bar/status-user-context-menu/status-user-context-menu.component.ts @@ -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(); + + @ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent; + + private accounts$: Observable; + 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 { + let statusPromise: Promise = 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; + } +}