From ec233754dda5e41f4b82b7d1a899f4641e6b3649 Mon Sep 17 00:00:00 2001 From: Rob Petti Date: Sat, 19 Nov 2022 10:16:31 -0700 Subject: [PATCH] add post edit functionality --- src/app/app.module.ts | 4 +- .../create-status/create-status.component.ts | 59 +++++++++++++++++-- .../edit-status/edit-status.component.html | 9 +++ .../edit-status/edit-status.component.scss | 37 ++++++++++++ .../edit-status/edit-status.component.spec.ts | 25 ++++++++ .../edit-status/edit-status.component.ts | 26 ++++++++ .../floating-column.component.html | 12 ++-- .../floating-column.component.ts | 11 ++++ .../status-user-context-menu.component.html | 5 +- .../status-user-context-menu.component.ts | 24 +++++++- src/app/services/mastodon-wrapper.service.ts | 15 +++-- src/app/services/mastodon.service.ts | 48 ++++++++++++++- src/app/services/models/api.settings.ts | 1 + src/app/services/navigation.service.ts | 11 +++- 14 files changed, 267 insertions(+), 20 deletions(-) create mode 100644 src/app/components/floating-column/edit-status/edit-status.component.html create mode 100644 src/app/components/floating-column/edit-status/edit-status.component.scss create mode 100644 src/app/components/floating-column/edit-status/edit-status.component.spec.ts create mode 100644 src/app/components/floating-column/edit-status/edit-status.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 582e1230..087a89e4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -40,6 +40,7 @@ import { SettingsComponent } from './components/floating-column/settings/setting import { AddNewAccountComponent } from './components/floating-column/add-new-account/add-new-account.component'; import { SearchComponent } from './components/floating-column/search/search.component'; import { AddNewStatusComponent } from "./components/floating-column/add-new-status/add-new-status.component"; +import { EditStatusComponent } from "./components/floating-column/edit-status/edit-status.component"; import { ManageAccountComponent } from "./components/floating-column/manage-account/manage-account.component"; import { ActionBarComponent } from './components/stream/status/action-bar/action-bar.component'; import { WaitingAnimationComponent } from './components/waiting-animation/waiting-animation.component'; @@ -111,6 +112,7 @@ const routes: Routes = [ FloatingColumnComponent, ManageAccountComponent, AddNewStatusComponent, + EditStatusComponent, AttachementsComponent, SettingsComponent, AddNewAccountComponent, @@ -173,7 +175,7 @@ const routes: Routes = [ FormsModule, ReactiveFormsModule, PickerModule, - OwlDateTimeModule, + OwlDateTimeModule, OwlNativeDateTimeModule, OverlayModule, RouterModule.forRoot(routes), diff --git a/src/app/components/create-status/create-status.component.ts b/src/app/components/create-status/create-status.component.ts index 0f3430d5..f2138d67 100644 --- a/src/app/components/create-status/create-status.component.ts +++ b/src/app/components/create-status/create-status.component.ts @@ -83,6 +83,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy { return s; } + @Input('statusToEdit') + set statusToEdit(value: StatusWrapper) { + if (value) { + this.isEditing = true; + this.editingId = value.status.id; + this.redraftedStatus = value; + } + } + @Input('redraftedStatus') set redraftedStatus(value: StatusWrapper) { if (value) { @@ -141,6 +150,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy { autosuggestData: string = null; instanceSupportsPoll = true; instanceSupportsScheduling = true; + isEditing: boolean; + editingId: string; private statusLoaded: boolean; private hasSuggestions: boolean; @@ -572,7 +583,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy { usableStatus .then((status: Status) => { - return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime); + if (this.isEditing) { + return this.editStatus(acc, this.editingId, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime); + } else { + return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime); + } }) .then((res: Status) => { this.title = ''; @@ -635,6 +650,42 @@ export class CreateStatusComponent implements OnInit, OnDestroy { return resultPromise; } + private editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise { + let parsedStatus = this.parseStatus(status); + let resultPromise = Promise.resolve(previousStatus); + + for (let i = 0; i < parsedStatus.length; i++) { + let s = parsedStatus[i]; + resultPromise = resultPromise + .then((pStatus: Status) => { + let inReplyToId = null; + if (pStatus) { + inReplyToId = pStatus.id; + } + + if (i === 0) { + return this.mastodonService.editStatus(account, statusId, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt) + .then((status: Status) => { + this.mediaService.clearMedia(); + return status; + }); + } else { + return this.mastodonService.editStatus(account, statusId, s, visibility, title, inReplyToId, [], null, scheduledAt); + } + }) + .then((status: Status) => { + if (this.statusReplyingToWrapper) { + let cwPolicy = this.toolsService.checkContentWarning(status); + this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(cwPolicy.status, account, cwPolicy.applyCw, cwPolicy.hide)); + } + + return status; + }); + } + + return resultPromise; + } + private parseStatus(status: string): string[] { //console.error(status.toString()); @@ -654,7 +705,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy { while (trucatedStatus.length > currentMaxCharLength) { const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars); - + if(nextIndex === -1){ break; } @@ -706,8 +757,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy { suggestionSelected(selection: AutosuggestSelection) { if (this.status.includes(selection.pattern)) { this.status = this.replacePatternWithAutosuggest(this.status, selection.pattern, selection.autosuggest); - - let cleanStatus = this.status.replace(/\r?\n/g, ' '); + + let cleanStatus = this.status.replace(/\r?\n/g, ' '); let newCaretPosition = cleanStatus.indexOf(`${selection.autosuggest}`) + selection.autosuggest.length; if (newCaretPosition > cleanStatus.length) newCaretPosition = cleanStatus.length; diff --git a/src/app/components/floating-column/edit-status/edit-status.component.html b/src/app/components/floating-column/edit-status/edit-status.component.html new file mode 100644 index 00000000..a14534ce --- /dev/null +++ b/src/app/components/floating-column/edit-status/edit-status.component.html @@ -0,0 +1,9 @@ +
+

edit message

+ +
+ + +
+
\ No newline at end of file diff --git a/src/app/components/floating-column/edit-status/edit-status.component.scss b/src/app/components/floating-column/edit-status/edit-status.component.scss new file mode 100644 index 00000000..b86a13ca --- /dev/null +++ b/src/app/components/floating-column/edit-status/edit-status.component.scss @@ -0,0 +1,37 @@ +@import "variables"; +@import "panel"; +@import "buttons"; +@import "commons"; + +$btn-send-status-width: 60px; + +.form-control { + margin-bottom: 5px; + + &--privacy{ + display: inline-block; + width: calc(100% - 5px - #{$btn-send-status-width}); + } +} + +.btn-custom-primary { + display: inline-block; + width: $btn-send-status-width; + position: relative; + top: -1px; + left: 5px; + font-weight: 500; +} + +.panel { + padding-left: 0; + padding-right: 0; +} + +.new-message-body { + overflow: auto; + height: calc(100% - 30px); + padding-right: 5px; + + padding-bottom: 100px; +} diff --git a/src/app/components/floating-column/edit-status/edit-status.component.spec.ts b/src/app/components/floating-column/edit-status/edit-status.component.spec.ts new file mode 100644 index 00000000..9ac902b6 --- /dev/null +++ b/src/app/components/floating-column/edit-status/edit-status.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { EditStatusComponent } from '../edit-status/edit-status.component'; + + +xdescribe('EditStatusComponent', () => { + let component: EditStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EditStatusComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/floating-column/edit-status/edit-status.component.ts b/src/app/components/floating-column/edit-status/edit-status.component.ts new file mode 100644 index 00000000..300c7c6f --- /dev/null +++ b/src/app/components/floating-column/edit-status/edit-status.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit, Input } from '@angular/core'; + +import { NavigationService } from '../../../services/navigation.service'; +import { StatusWrapper } from '../../../models/common.model'; + +@Component({ + selector: 'app-edit-status', + templateUrl: './edit-status.component.html', + styleUrls: ['./edit-status.component.scss'] +}) +export class EditStatusComponent implements OnInit { + + @Input() isDirectMention: boolean; + @Input() userHandle: string; + @Input() statusToEdit: StatusWrapper; + + constructor(private readonly navigationService: NavigationService) { + } + + ngOnInit() { + } + + closeColumn() { + this.navigationService.closePanel(); + } +} diff --git a/src/app/components/floating-column/floating-column.component.html b/src/app/components/floating-column/floating-column.component.html index 330e49fd..9e293d72 100644 --- a/src/app/components/floating-column/floating-column.component.html +++ b/src/app/components/floating-column/floating-column.component.html @@ -1,9 +1,9 @@
- @@ -15,15 +15,17 @@
+ - diff --git a/src/app/components/floating-column/floating-column.component.ts b/src/app/components/floating-column/floating-column.component.ts index bd947293..88fce265 100644 --- a/src/app/components/floating-column/floating-column.component.ts +++ b/src/app/components/floating-column/floating-column.component.ts @@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy { isDirectMention: boolean; userHandle: string; redraftedStatus: StatusWrapper; + statusToEdit: StatusWrapper; openPanel: string = ''; @@ -58,6 +59,16 @@ export class FloatingColumnComponent implements OnInit, OnDestroy { this.openPanel = 'createNewStatus'; } break; + case LeftPanelType.EditStatus: + if (this.openPanel === 'editStatus') { + this.closePanel(); + } else { + this.isDirectMention = event.action === LeftPanelAction.DM; + this.userHandle = event.userHandle; + this.statusToEdit = event.status; + this.openPanel = 'editStatus'; + } + break; case LeftPanelType.ManageAccount: let lastUserAccountId = ''; if (this.userAccountUsed && this.userAccountUsed.info) { 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 index f58473a2..d91d0d60 100644 --- 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 @@ -1,4 +1,4 @@ - @@ -40,6 +40,9 @@ Unpin from profile + + Edit + Delete 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 index a726539b..85744157 100644 --- 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 @@ -5,7 +5,7 @@ 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 { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../../services/tools.service'; import { StatusWrapper } from '../../../../../models/common.model'; import { NavigationService } from '../../../../../services/navigation.service'; import { AccountInfo } from '../../../../../states/accounts.state'; @@ -27,6 +27,8 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy { username: string; isOwnerSelected: boolean; + isEditingAvailable: boolean; + @Input() statusWrapper: StatusWrapper; @Input() displayedAccount: Account; @@ -78,6 +80,14 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy { this.isOwnerSelected = selectedAccount.username.toLowerCase() === this.displayedStatus.account.username.toLowerCase() && selectedAccount.instance.toLowerCase() === this.displayedStatus.account.url.replace('https://', '').split('/')[0].toLowerCase(); + + this.toolsService.getInstanceInfo(selectedAccount).then((instanceInfo: InstanceInfo) => { + if (instanceInfo.major >= 4) { + this.isEditingAvailable = true; + } else { + this.isEditingAvailable = false; + } + }); } @@ -282,6 +292,18 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy { return false; } + edit(): boolean { + const selectedAccount = this.toolsService.getSelectedAccounts()[0]; + this.getStatus(selectedAccount) + .then(() => { + this.navigationService.edit(this.statusWrapper); + }) + .catch(err => { + this.notificationService.notifyHttpError(err, selectedAccount); + }); + return false; + } + private getStatus(account: AccountInfo): Promise { let statusPromise: Promise = Promise.resolve(this.statusWrapper.status); diff --git a/src/app/services/mastodon-wrapper.service.ts b/src/app/services/mastodon-wrapper.service.ts index 63e0b689..f4675380 100644 --- a/src/app/services/mastodon-wrapper.service.ts +++ b/src/app/services/mastodon-wrapper.service.ts @@ -12,7 +12,7 @@ import { SettingsService } from './settings.service'; @Injectable({ providedIn: 'root' }) -export class MastodonWrapperService { +export class MastodonWrapperService { private refreshingToken: { [id: string]: Promise } = {}; constructor( @@ -29,7 +29,7 @@ export class MastodonWrapperService { let isExpired = false; let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id); - if(!storedAccountInfo || !(storedAccountInfo.token)) + if(!storedAccountInfo || !(storedAccountInfo.token)) return Promise.resolve(accountInfo); try { @@ -39,7 +39,7 @@ export class MastodonWrapperService { } else { const nowEpoch = Date.now() / 1000 | 0; - //Pleroma workaround + //Pleroma workaround let expire_in = storedAccountInfo.token.expires_in; if (expire_in < 3600) { expire_in = 3600; @@ -74,7 +74,7 @@ export class MastodonWrapperService { p.then(() => { this.refreshingToken[accountInfo.id] = null; }); - + this.refreshingToken[accountInfo.id] = p; return p; } else { @@ -124,6 +124,13 @@ export class MastodonWrapperService { }); } + editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise { + return this.refreshAccountIfNeeded(account) + .then((refreshedAccount: AccountInfo) => { + return this.mastodonService.editStatus(refreshedAccount, statusId, status, visibility, spoiler, in_reply_to_id, mediaIds, poll, scheduled_at); + }); + } + getStatus(account: AccountInfo, statusId: string): Promise { return this.refreshAccountIfNeeded(account) .then((refreshedAccount: AccountInfo) => { diff --git a/src/app/services/mastodon.service.ts b/src/app/services/mastodon.service.ts index 4c5c7a3f..46609a83 100644 --- a/src/app/services/mastodon.service.ts +++ b/src/app/services/mastodon.service.ts @@ -128,6 +128,50 @@ export class MastodonService { return this.httpClient.post(url, statusData, { headers: headers }).toPromise(); } + editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise { + const url = `https://${account.instance}${this.apiRoutes.editStatus.replace('{0}', statusId)}`; + + const statusData = new StatusData(); + statusData.status = status; + statusData.media_ids = mediaIds; + + if (poll) { + statusData['poll'] = poll; + } + + if (scheduled_at) { + statusData['scheduled_at'] = scheduled_at; + } + + if (in_reply_to_id) { + statusData.in_reply_to_id = in_reply_to_id; + } + if (spoiler) { + statusData.sensitive = true; + statusData.spoiler_text = spoiler; + } + switch (visibility) { + case VisibilityEnum.Public: + statusData.visibility = 'public'; + break; + case VisibilityEnum.Unlisted: + statusData.visibility = 'unlisted'; + break; + case VisibilityEnum.Private: + statusData.visibility = 'private'; + break; + case VisibilityEnum.Direct: + statusData.visibility = 'direct'; + break; + default: + statusData.visibility = 'private'; + break; + } + + const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); + return this.httpClient.put(url, statusData, { headers: headers }).toPromise(); + } + getStatus(account: AccountInfo, statusId: string): Promise { const route = `https://${account.instance}${this.apiRoutes.getStatus.replace('{0}', statusId)}`; const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); @@ -382,10 +426,10 @@ export class MastodonService { addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise { let route = `https://${account.instance}${this.apiRoutes.addAccountToList}`.replace('{0}', listId); route += `?account_ids[]=${accountId}`; - + let data = new ListAccountData(); data.account_ids.push(accountId.toString()); - + const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); return this.httpClient.post(route, data, { headers: headers }).toPromise(); } diff --git a/src/app/services/models/api.settings.ts b/src/app/services/models/api.settings.ts index 08573c16..8fc41e7a 100644 --- a/src/app/services/models/api.settings.ts +++ b/src/app/services/models/api.settings.ts @@ -41,6 +41,7 @@ export class ApiRoutes { getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by'; getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by'; postNewStatus = '/api/v1/statuses'; + editStatus = '/api/v1/statuses/{0}'; deleteStatus = '/api/v1/statuses/{0}'; reblogStatus = '/api/v1/statuses/{0}/reblog'; unreblogStatus = '/api/v1/statuses/{0}/unreblog'; diff --git a/src/app/services/navigation.service.ts b/src/app/services/navigation.service.ts index 9b39a587..72570b78 100644 --- a/src/app/services/navigation.service.ts +++ b/src/app/services/navigation.service.ts @@ -9,7 +9,7 @@ export class NavigationService { private accountToManage: AccountWrapper; activatedPanelSubject = new BehaviorSubject(new OpenLeftPanelEvent(LeftPanelType.Closed)); activatedMediaSubject: Subject = new Subject(); - columnSelectedSubject = new BehaviorSubject(-1); + columnSelectedSubject = new BehaviorSubject(-1); constructor() { } @@ -41,6 +41,11 @@ export class NavigationService { this.activatedPanelSubject.next(newEvent); } + edit(status: StatusWrapper){ + const newEvent = new OpenLeftPanelEvent(LeftPanelType.EditStatus, LeftPanelAction.Edit, null, status); + this.activatedPanelSubject.next(newEvent); + } + columnSelected(index: number): void { this.columnSelectedSubject.next(index); } @@ -68,6 +73,7 @@ export enum LeftPanelAction { DM = 1, Mention = 2, Redraft = 3, + Edit = 4, } export enum LeftPanelType { @@ -77,5 +83,6 @@ export enum LeftPanelType { Search = 3, AddNewAccount = 4, Settings = 5, - ScheduledStatuses = 6 + ScheduledStatuses = 6, + EditStatus = 7, } \ No newline at end of file