1
0
mirror of https://github.com/NicolasConstant/sengi synced 2025-02-07 15:38:42 +01:00

Merge pull request #517 from NicolasConstant/develop

1.2.0 PR
This commit is contained in:
Nicolas Constant 2022-12-10 22:53:48 -05:00 committed by GitHub
commit 4599d64c60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 400 additions and 90 deletions

View File

@ -3,7 +3,7 @@ cache:
#- node_modules
environment:
GH_TOKEN:
secure: wRRBU0GXTmTBgZBs2PGSaEJWOflynAyvp3Nc/7e9xmciPfkUCQAXcpOn0jIYmzpb
secure: eXSiJiDFgLi4vixO5GS93lgrqZ+BzQNy7PKPCQCErHjCQD9mWiEtVQQnhvmUq1FPLUc3fNLmOFQu2nIWA9bnkHg5Yw9WiG2m7QSCPRB+xCnvSY6JbLqpzURZp5x5OLj6
matrix:
- nodejs_version: 10.9.0
install:

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "1.1.6",
"version": "1.2.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",

View File

@ -173,7 +173,7 @@ const routes: Routes = [
FormsModule,
ReactiveFormsModule,
PickerModule,
OwlDateTimeModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
RouterModule.forRoot(routes),

View File

@ -28,6 +28,7 @@ export abstract class TimelineBase extends BrowseBase {
statuses: StatusWrapper[] = [];
bufferStream: Status[] = [];
protected bufferWasCleared: boolean;
numNewItems: number;
streamPositionnedAtTop: boolean = true;
protected isProcessingInfiniteScroll: boolean;

View File

@ -27,13 +27,15 @@
<div class="status-editor__footer" #footer>
<button type="submit" title="reply" class="status-editor__footer--send-button" *ngIf="statusReplyingToWrapper">
<span *ngIf="!isSending && !scheduleIsActive">REPLY!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">REPLY!</span>
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
<span *ngIf="!isSending && isEditing">EDIT!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<button type="submit" title="post" class="status-editor__footer--send-button" *ngIf="!statusReplyingToWrapper">
<span *ngIf="!isSending && !scheduleIsActive">POST!</span>
<span *ngIf="!isSending && scheduleIsActive">PLAN!</span>
<span *ngIf="!isSending && !scheduleIsActive && !isEditing">POST!</span>
<span *ngIf="!isSending && scheduleIsActive && !isEditing">PLAN!</span>
<span *ngIf="!isSending && isEditing">EDIT!</span>
<app-waiting-animation class="waiting-icon" *ngIf="isSending"></app-waiting-animation>
</button>
<div class="status-editor__footer__counter">

View File

@ -154,6 +154,9 @@ $counter-width: 90px;
}
& span {
position: relative;
top: 1px;
margin: 0;
padding: 0;
}

View File

@ -83,12 +83,21 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return s;
}
@Input('statusToEdit')
set statusToEdit(value: StatusWrapper) {
if (value) {
this.isEditing = true;
this.editingStatusId = value.status.id;
this.redraftedStatus = value;
}
}
@Input('redraftedStatus')
set redraftedStatus(value: StatusWrapper) {
if (value) {
this.isRedrafting = true;
this.statusLoaded = false;
if (value.status && value.status.media_attachments) {
for (const m of value.status.media_attachments) {
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
@ -141,6 +150,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
autosuggestData: string = null;
instanceSupportsPoll = true;
instanceSupportsScheduling = true;
isEditing: boolean;
editingStatusId: string;
private statusLoaded: boolean;
private hasSuggestions: boolean;
@ -198,7 +209,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private readonly instancesInfoService: InstancesInfoService,
private readonly mediaService: MediaService,
private readonly overlay: Overlay,
public viewContainerRef: ViewContainerRef) {
public viewContainerRef: ViewContainerRef,
private readonly statusesStateService: StatusesStateService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
@ -308,7 +320,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
};
const word = this.getWordByPos(currentSection, caretPosition - offset);
if (!lastCharIsSpace && word && word.length > 0 && (word.startsWith('@') || word.startsWith('#'))) {
if (!lastCharIsSpace && word && word.length > 1 && (word.startsWith('@') || word.startsWith('#'))) {
this.autosuggestData = word;
return;
}
@ -436,7 +448,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
private setVisibility(defaultPrivacy: VisibilityEnum) {
if(this.selectedPrivacySetByRedraft) return;
if (this.selectedPrivacySetByRedraft) return;
switch (defaultPrivacy) {
case VisibilityEnum.Public:
@ -494,14 +506,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private getMentions(status: Status): string[] {
let acct = status.account.acct;
if(!acct.includes('@')) {
if (!acct.includes('@')) {
acct += `@${status.account.url.replace('https://', '').split('/')[0]}`
}
const mentions = [acct];
status.mentions.forEach(m => {
let mentionAcct = m.acct;
if(!mentionAcct.includes('@')){
if (!mentionAcct.includes('@')) {
mentionAcct += `@${m.url.replace('https://', '').split('/')[0]}`;
}
mentions.push(mentionAcct);
@ -572,7 +584,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
usableStatus
.then((status: Status) => {
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime);
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments, poll, scheduledTime, this.editingStatusId);
})
.then((res: Status) => {
this.title = '';
@ -599,7 +611,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return false;
}
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string): Promise<Status> {
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string, editingStatusId: string): Promise<Status> {
let parsedStatus = this.parseStatus(status);
let resultPromise = Promise.resolve(previousStatus);
@ -613,13 +625,25 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
if (i === 0) {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt)
let postPromise: Promise<Status>;
if (this.isEditing) {
postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt);
} else {
postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt);
}
return postPromise
.then((status: Status) => {
this.mediaService.clearMedia();
return status;
});
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
if (this.isEditing) {
return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt);
} else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt);
}
}
})
.then((status: Status) => {
@ -628,6 +652,16 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.notificationService.newStatusPosted(this.statusReplyingToWrapper.status.id, new StatusWrapper(cwPolicy.status, account, cwPolicy.applyCw, cwPolicy.hide));
}
return status;
})
.then((status: Status) => {
if (this.isEditing) {
let cwPolicy = this.toolsService.checkContentWarning(status);
let statusWrapper = new StatusWrapper(status, account, cwPolicy.applyCw, cwPolicy.hide);
this.statusesStateService.statusEditedStatusChanged(status.url, account.id, statusWrapper);
}
return status;
});
}
@ -636,8 +670,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
private parseStatus(status: string): string[] {
//console.error(status.toString());
let mentionExtraChars = this.getMentionExtraChars(status);
let urlExtraChar = this.getLinksExtraChars(status);
let trucatedStatus = `${status}`;
@ -654,8 +686,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
while (trucatedStatus.length > currentMaxCharLength) {
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
if(nextIndex === -1){
if (nextIndex === -1) {
break;
}
@ -706,8 +738,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;
@ -756,7 +788,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
w++;
result += `${word}`;
if(w < wordCount || i === nberLines){
if (w < wordCount || i === nberLines) {
result += ' ';
}
});
@ -768,7 +800,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
result = result.replace(' ', ' ');
let endRegex = new RegExp(`${autosuggest} $`, 'i');
if(!result.match(endRegex)){
if (!result.match(endRegex)) {
result = result.substring(0, result.length - 1);
}

View File

@ -3,7 +3,6 @@
<div class=" new-message-body flexcroll">
<app-create-status (onClose)="closeColumn()" [isDirectMention]="isDirectMention"
[replyingUserHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-create-status>
[replyingUserHandle]="userHandle" [statusToEdit]="statusToEdit" [redraftedStatus]="redraftedStatus"></app-create-status>
</div>
</div>

View File

@ -13,6 +13,7 @@ export class AddNewStatusComponent implements OnInit {
@Input() isDirectMention: boolean;
@Input() userHandle: string;
@Input() redraftedStatus: StatusWrapper;
@Input() statusToEdit: StatusWrapper;
constructor(private readonly navigationService: NavigationService) {
}

View File

@ -1,9 +1,9 @@
<div class="floating-column">
<div class="floating-column__inner">
<div class="sliding-column" [class.sliding-column__right-display]="overlayActive">
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
(closeOverlay)="closeOverlay()"
[browseAccountData]="overlayAccountToBrowse"
[browseAccountData]="overlayAccountToBrowse"
[browseHashtagData]="overlayHashtagToBrowse"
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
@ -15,15 +15,17 @@
</div>
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"
(browseAccountEvent)="browseAccount($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-manage-account>
<app-add-new-status *ngIf="openPanel === 'createNewStatus'" [isDirectMention]="isDirectMention"
[userHandle]="userHandle" [redraftedStatus]="redraftedStatus"></app-add-new-status>
[userHandle]="userHandle"
[redraftedStatus]="redraftedStatus"
[statusToEdit]="statusToEdit"></app-add-new-status>
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
<app-search *ngIf="openPanel === 'search'"
<app-search *ngIf="openPanel === 'search'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)">
</app-search>
<app-settings *ngIf="openPanel === 'settings'"></app-settings>

View File

@ -25,6 +25,7 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
isDirectMention: boolean;
userHandle: string;
redraftedStatus: StatusWrapper;
statusToEdit: StatusWrapper;
openPanel: string = '';
@ -49,12 +50,21 @@ export class FloatingColumnComponent implements OnInit, OnDestroy {
}
break;
case LeftPanelType.CreateNewStatus:
case LeftPanelType.EditStatus:
if (this.openPanel === 'createNewStatus' && !event.userHandle) {
this.closePanel();
} else {
this.isDirectMention = event.action === LeftPanelAction.DM;
this.userHandle = event.userHandle;
this.redraftedStatus = event.status;
if(event.type === LeftPanelType.CreateNewStatus){
this.redraftedStatus = event.status;
this.statusToEdit = null;
} else {
this.redraftedStatus = null;
this.statusToEdit = event.status;
}
this.openPanel = 'createNewStatus';
}
break;

View File

@ -2,15 +2,16 @@
<div class="hashtag-header">
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
<button *ngIf="isHashtagFollowingAvailable && !isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="followThisHashtag($event)" title="follow hashtag" [disabled]="followingLoading">follow</button>
<button *ngIf="isHashtagFollowingAvailable && isFollowingHashtag" class="btn-custom-secondary hashtag-header__follow-button" (click)="unfollowThisHashtag($event)" title="unfollow hashtag" [disabled]="unfollowingLoading">unfollow</button>
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board" [hidden]="columnAdded">add column</button>
</a>
</div>
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
[streamElement]="hashtagElement"
<app-stream-statuses #appStreamStatuses class="hashtag-stream" *ngIf="hashtagElement"
[streamElement]="hashtagElement"
[goToTop]="goToTopSubject.asObservable()"
[userLocked]="false"
(browseAccountEvent)="browseAccount($event)"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-stream-statuses>
</div>

View File

@ -40,6 +40,14 @@ $inner-column-size: 320px;
border: 1px solid black;
color: white;
}
&__follow-button {
position: absolute;
top: 7px;
right: 100px;
padding: 0 10px 0 10px;
border: 1px solid black;
color: white;
}
}
.hashtag-stream {

View File

@ -1,11 +1,12 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { Subject, Subscription, Observable } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
import { AccountInfo } from '../../../states/accounts.state';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
@Component({
selector: 'app-hashtag',
@ -21,7 +22,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
private _hashtagElement: StreamElement;
@Input()
@Input()
set hashtagElement(hashtagElement: StreamElement){
this._hashtagElement = hashtagElement;
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
@ -29,7 +30,7 @@ export class HashtagComponent implements OnInit, OnDestroy {
get hashtagElement(): StreamElement{
return this._hashtagElement;
}
@ViewChild('appStreamStatuses') appStreamStatuses: StreamStatusesComponent;
@ -38,12 +39,25 @@ export class HashtagComponent implements OnInit, OnDestroy {
private lastUsedAccount: AccountInfo;
private refreshSubscription: Subscription;
private goToTopSubscription: Subscription;
isHashtagFollowingAvailable: boolean;
isFollowingHashtag: boolean;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
followingLoading: boolean;
unfollowingLoading: boolean;
columnAdded: boolean;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
if(this.refreshEventEmitter) {
@ -57,11 +71,22 @@ export class HashtagComponent implements OnInit, OnDestroy {
this.goToTop();
})
}
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
const selectedAccounts = accounts.filter(x => x.isSelected);
if (selectedAccounts.length > 0) {
this.lastUsedAccount = selectedAccounts[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
}
});
}
ngOnDestroy(): void {
if(this.refreshSubscription) this.refreshSubscription.unsubscribe();
if (this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
if (this.accountSub) this.accountSub.unsubscribe();
}
goToTop(): boolean {
@ -83,6 +108,10 @@ export class HashtagComponent implements OnInit, OnDestroy {
refresh(): any {
this.lastUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.updateHashtagFollowStatus(this.lastUsedAccount);
if (this.isHashtagFollowingAvailable) {
this.checkIfFollowingHashtag(this.lastUsedAccount);
}
this.appStreamStatuses.refresh();
}
@ -99,4 +128,41 @@ export class HashtagComponent implements OnInit, OnDestroy {
browseThread(openThreadEvent: OpenThreadEvent): void {
this.browseThreadEvent.next(openThreadEvent);
}
private updateHashtagFollowStatus(account: AccountInfo): void {
this.toolsService.getInstanceInfo(account).then(instanceInfo => {
if (instanceInfo.major >= 4) {
this.isHashtagFollowingAvailable = true;
this.checkIfFollowingHashtag(account);
} else {
this.isHashtagFollowingAvailable = false;
}
});
}
private checkIfFollowingHashtag(account: AccountInfo): void {
this.mastodonService.getHashtag(account, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
});
}
followThisHashtag(event): boolean {
this.followingLoading = true;
event.stopPropagation();
this.mastodonService.followHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
this.followingLoading = false;
});
return false
}
unfollowThisHashtag(event): boolean {
this.unfollowingLoading = true;
event.stopPropagation();
this.mastodonService.unfollowHashtag(this.lastUsedAccount, this.hashtagElement.tag).then(tag => {
this.isFollowingHashtag = tag.following;
this.unfollowingLoading = false;
});
return false
}
}

View File

@ -1,4 +1,4 @@
<a href class="context-menu-link" (click)="onContextMenu($event)"
<a href class="context-menu-link" (click)="onContextMenu($event)"
[class.context-menu-link__status]="statusWrapper"
[class.context-menu-link__profile]="displayedAccount"
title="More">
@ -40,6 +40,9 @@
<ng-template contextMenuItem (execute)="unpinFromProfile()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.pinned && displayedStatus.visibility === 'public'">
Unpin from profile
</ng-template>
<ng-template contextMenuItem (execute)="edit()" *ngIf="statusWrapper && isOwnerSelected && isEditingAvailable">
Edit
</ng-template>
<ng-template contextMenuItem (execute)="delete(false)" *ngIf="statusWrapper && isOwnerSelected">
Delete
</ng-template>

View File

@ -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<Status> {
let statusPromise: Promise<Status> = Promise.resolve(this.statusWrapper.status);

View File

@ -85,6 +85,9 @@
<div class="status__labels--label status__labels--remote" title="this status isn't federated with this instance" *ngIf="isRemote">
remote
</div>
<div class="status__labels--label status__labels--edited" title="this status was edited" *ngIf="statusWrapper.status.edited_at">
edited
</div>
</div>

View File

@ -105,6 +105,17 @@
background-color: rgb(33, 69, 136);
background-color: rgb(38, 77, 148);
}
&--edited {
background-color: rgb(167, 0, 153);
background-color: rgb(0, 128, 167);
background-color: rgb(65, 65, 71);
background-color: rgb(144, 184, 0);
background-color: rgb(82, 105, 0);
background-color: rgb(95, 95, 95);
// color: black;
}
}
&__name {
display: inline-block;

View File

@ -1,5 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core";
import { faStar, faRetweet, faList, faThumbtack } from "@fortawesome/free-solid-svg-icons";
import { Subscription } from "rxjs";
import { Status, Account } from "../../../services/models/mastodon.interfaces";
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
@ -7,7 +8,8 @@ import { ActionBarComponent } from "./action-bar/action-bar.component";
import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { ContentWarningPolicyEnum } from '../../../states/settings.state';
import { stat } from 'fs';
import { StatusesStateService, StatusState } from "../../../services/statuses-state.service";
@Component({
selector: "app-status",
@ -56,6 +58,8 @@ export class StatusComponent implements OnInit {
private _statusWrapper: StatusWrapper;
status: Status;
private statusesStateServiceSub: Subscription;
@Input('statusWrapper')
set statusWrapper(value: StatusWrapper) {
this._statusWrapper = value;
@ -97,9 +101,19 @@ export class StatusComponent implements OnInit {
constructor(
public elem: ElementRef,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService,
private readonly statusesStateService: StatusesStateService) { }
ngOnInit() {
this.statusesStateServiceSub = this.statusesStateService.stateNotification.subscribe(notification => {
if(this._statusWrapper.status.url === notification.statusId && notification.isEdited) {
this.statusWrapper = notification.editedStatus;
}
});
}
ngOnDestroy(){
if(this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
}
private ensureMentionAreDisplayed(data: string): string {

View File

@ -5,18 +5,18 @@
</a>
</div>
<div class="stream-toots__new-notification"
<div class="stream-toots__new-notification"
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0 && !streamPositionnedAtTop"></div>
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
<div *ngIf="timelineLoadingMode === 3 && bufferStream && bufferStream.length > 0">
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ bufferStream.length }} new item<span *ngIf="bufferStream.length > 1">s</span></a>
<div *ngIf="timelineLoadingMode === 3 && bufferStream && numNewItems > 0">
<a href (click)="loadBuffer()" class="stream-toots__load-buffer" title="load new items">{{ numNewItems }} new item<span *ngIf="numNewItems > 1">s</span></a>
</div>
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
<app-status
<app-status
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>

View File

@ -101,6 +101,8 @@ export class StreamStatusesComponent extends TimelineBase {
});
}
});
this.numNewItems = 0;
}
ngOnDestroy() {
@ -133,6 +135,7 @@ export class StreamStatusesComponent extends TimelineBase {
private resetStream() {
this.statuses.length = 0;
this.bufferStream.length = 0;
this.numNewItems = 0;
if (this.websocketStreaming) this.websocketStreaming.dispose();
}
@ -154,6 +157,7 @@ export class StreamStatusesComponent extends TimelineBase {
this.statuses.unshift(wrapper);
} else {
this.bufferStream.push(update.status);
this.numNewItems++;
}
}
} else if (update.type === EventEnum.delete) {
@ -201,6 +205,7 @@ export class StreamStatusesComponent extends TimelineBase {
}
this.bufferStream.length = 0;
this.numNewItems = 0;
return false;
}
@ -212,7 +217,7 @@ export class StreamStatusesComponent extends TimelineBase {
return status.filter(x => !this.isFiltered(x));
});
}
private isFiltered(status: Status): boolean {
if (this.streamElement.hideBoosts) {
if (status.reblog) {

View File

@ -28,6 +28,7 @@ export class ThreadComponent extends BrowseBase {
hasContentWarnings = false;
private remoteStatusFetchingDisabled = false;
numNewItems: number; //html compatibility only
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only
timelineLoadingMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; //html compatibility only

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData, Tag } from "./models/mastodon.interfaces";
import { AccountInfo, UpdateAccount } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
@ -12,7 +12,7 @@ import { SettingsService } from './settings.service';
@Injectable({
providedIn: 'root'
})
export class MastodonWrapperService {
export class MastodonWrapperService {
private refreshingToken: { [id: string]: Promise<AccountInfo> } = {};
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<Status> {
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<Status> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -267,6 +274,27 @@ export class MastodonWrapperService {
});
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.followHashtag(refreshedAccount, hashtag);
});
}
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unfollowHashtag(refreshedAccount, hashtag);
});
}
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getHashtag(refreshedAccount, hashtag);
});
}
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, Tag } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state';
@ -128,6 +128,50 @@ export class MastodonService {
return this.httpClient.post<Status>(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<Status> {
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<Status>(url, statusData, { headers: headers }).toPromise();
}
getStatus(account: AccountInfo, statusId: string): Promise<Status> {
const route = `https://${account.instance}${this.apiRoutes.getStatus.replace('{0}', statusId)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
@ -289,6 +333,24 @@ export class MastodonService {
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.followHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Tag>(route, null, { headers: headers }).toPromise();
}
unfollowHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.unfollowHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Tag>(route, null, { headers: headers }).toPromise();
}
getHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.getHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.get<Tag>(route, { headers: headers }).toPromise();
}
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
let input = new FormData();
input.append('file', file);
@ -382,10 +444,10 @@ export class MastodonService {
addAccountToList(account: AccountInfo, listId: string, accountId: number): Promise<any> {
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();
}

View File

@ -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';
@ -75,4 +76,7 @@ export class ApiRoutes {
getBookmarks = '/api/v1/bookmarks';
getFollowers = '/api/v1/accounts/{0}/followers';
getFollowing = '/api/v1/accounts/{0}/following';
followHashtag = '/api/v1/tags/{0}/follow';
unfollowHashtag = '/api/v1/tags/{0}/unfollow';
getHashtag = '/api/v1/tags/{0}';
}

View File

@ -141,7 +141,7 @@ export interface Relationship {
id: number;
following: boolean;
followed_by: boolean;
blocked_by: boolean;
blocked_by: boolean;
blocking: boolean;
domain_blocking: boolean;
muting: boolean;
@ -172,6 +172,7 @@ export interface Status {
reblog: Status;
content: string;
created_at: string;
edited_at: string;
reblogs_count: number;
replies_count: number;
favourites_count: string;
@ -190,7 +191,7 @@ export interface Status {
muted: boolean;
bookmarked: boolean;
card: Card;
poll: Poll;
poll: Poll;
pleroma: PleromaStatusInfo;
}
@ -207,11 +208,6 @@ export interface PleromaStatusInfo {
local: boolean;
}
export interface Tag {
name: string;
url: string;
}
export interface List {
id: string;
title: string;
@ -249,4 +245,17 @@ export interface StatusParams {
visibility: 'public' | 'unlisted' | 'private' | 'direct';
scheduled_at: string;
application_id: string;
}
export interface TagHistory {
day: string;
uses: number;
accounts: number;
}
export interface Tag {
name: string;
url: string;
history: TagHistory[];
following: boolean;
}

View File

@ -9,7 +9,7 @@ export class NavigationService {
private accountToManage: AccountWrapper;
activatedPanelSubject = new BehaviorSubject<OpenLeftPanelEvent>(new OpenLeftPanelEvent(LeftPanelType.Closed));
activatedMediaSubject: Subject<OpenMediaEvent> = new Subject<OpenMediaEvent>();
columnSelectedSubject = new BehaviorSubject<number>(-1);
columnSelectedSubject = new BehaviorSubject<number>(-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,
}

View File

@ -6,15 +6,15 @@ import { StatusWrapper } from '../models/common.model';
@Injectable({
providedIn: 'root'
})
export class StatusesStateService {
private cachedStatusText: { [statusId: string]: string } = {};
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
export class StatusesStateService {
private cachedStatusText: { [statusId: string]: string } = {};
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
public stateNotification = new Subject<StatusState>();
constructor() { }
getStateForStatus(statusId: string): StatusState[] {
if(!this.cachedStatusStates[statusId])
if (!this.cachedStatusStates[statusId])
return null;
let results: StatusState[] = [];
@ -31,7 +31,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, isFavorited, null, null);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, isFavorited, null, null, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isFavorited = isFavorited;
}
@ -44,7 +44,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, isRebloged, null);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, isRebloged, null, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isRebloged = isRebloged;
}
@ -57,7 +57,7 @@ export class StatusesStateService {
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, isBookmarked);
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, isBookmarked, null, null);
} else {
this.cachedStatusStates[statusId][accountId].isBookmarked = isBookmarked;
}
@ -65,42 +65,58 @@ export class StatusesStateService {
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
}
setStatusContent(data: string, replyingToStatus: StatusWrapper){
if(replyingToStatus){
statusEditedStatusChanged(statusId: string, accountId: string, editedStatus: StatusWrapper) {
if (!this.cachedStatusStates[statusId])
this.cachedStatusStates[statusId] = {};
if (!this.cachedStatusStates[statusId][accountId]) {
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, null, null, null, true, editedStatus);
} else {
this.cachedStatusStates[statusId][accountId].isEdited = true;
this.cachedStatusStates[statusId][accountId].editedStatus = editedStatus;
}
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
}
setStatusContent(data: string, replyingToStatus: StatusWrapper) {
if (replyingToStatus) {
this.cachedStatusText[replyingToStatus.status.uri] = data;
} else {
this.cachedStatusText['none'] = data;
}
}
}
getStatusContent(replyingToStatus: StatusWrapper): string{
getStatusContent(replyingToStatus: StatusWrapper): string {
let data: string;
if(replyingToStatus){
if (replyingToStatus) {
data = this.cachedStatusText[replyingToStatus.status.uri];
} else {
data = this.cachedStatusText['none'];
}
if(!data) return '';
if (!data) return '';
return data;
}
resetStatusContent(replyingToStatus: StatusWrapper){
if(replyingToStatus){
resetStatusContent(replyingToStatus: StatusWrapper) {
if (replyingToStatus) {
this.cachedStatusText[replyingToStatus.status.uri] = '';
} else {
this.cachedStatusText['none'] = '';
}
}
}
}
export class StatusState {
constructor(
public statusId: string,
public accountId: string,
public isFavorited: boolean,
public statusId: string,
public accountId: string,
public isFavorited: boolean,
public isRebloged: boolean,
public isBookmarked: boolean) {
public isBookmarked: boolean,
public isEdited: boolean,
public editedStatus: StatusWrapper) {
}
}