Merge pull request #295 from NicolasConstant/develop

Develop PR
This commit is contained in:
Nicolas Constant 2020-05-29 03:40:04 -04:00 committed by GitHub
commit 9de28fad86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 42 deletions

View File

@ -38,11 +38,15 @@ function createWindow() {
{ type: "separator" },
{ role: "reload" },
{ role: "forcereload" },
{ type: 'separator' },
{ role: 'togglefullscreen' },
{ type: "separator" },
{ role: "resetzoom" },
{ role: "zoomin", accelerator: "CommandOrControl+numadd" },
{ role: "zoomout", accelerator: "CommandOrControl+numsub" },
{ type: "separator" },
{ role: "togglefullscreen" },
{ type: "separator" },
{ role: "close" },
{ role: 'quit' }
{ role: "quit" }
]
},
{
@ -191,4 +195,4 @@ app.on("activate", () => {
if (win === null) {
createWindow();
}
});
});

View File

@ -1,11 +1,11 @@
{
"name": "sengi",
"version": "0.28.1",
"version": "0.29.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
"author": {
"name": "Nicolas Constant",
"name": "Nicolas Constant",
"email": "github@nicolas-constant.com"
},
"repository": {

View File

@ -41,6 +41,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
faClock = faClock;
autoSuggestUserActionsStream = new EventEmitter<AutosuggestUserActionEnum>();
private isRedrafting: boolean;
private _title: string;
set title(value: string) {
@ -54,7 +55,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private _status: string = '';
@Input('status')
set status(value: string) {
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
if (this.isRedrafting) {
this.statusStateService.setStatusContent(value, null);
} else {
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
}
this.countStatusChar(value);
this.detectAutosuggestion(value);
this._status = value;
@ -79,31 +84,32 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
@Input('redraftedStatus')
set redraftedStatus(value: StatusWrapper) {
if (value) {
if (value) {
this.isRedrafting = true;
this.statusLoaded = false;
if(value.status && value.status.media_attachments){
if (value.status && value.status.media_attachments) {
for (const m of value.status.media_attachments) {
this.mediaService.addExistingMedia(new MediaWrapper(m.id, null, m));
}
}
const newLine = String.fromCharCode(13, 10);
let content = value.status.content;
content = this.tranformHtmlRepliesToReplies(content);
while (content.includes('<p>') || content.includes('</p>') || content.includes('<br>') || content.includes('<br/>') || content.includes('<br />')) {
content = content.replace('<p>', '').replace('</p>', newLine + newLine).replace('<br />', newLine).replace('<br/>', newLine).replace('<br>', newLine);
}
content = this.trim(content, newLine);
let parser = new DOMParser();
var dom = parser.parseFromString(content, 'text/html')
this.status = dom.body.textContent;
this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
// this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
this.setVisibilityFromStatus(value.status);
this.title = value.status.spoiler_text;
@ -193,10 +199,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
public viewContainerRef: ViewContainerRef) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
}
ngOnInit() {
if (!this.isRedrafting) {
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
}
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.accountChanged(accounts);
});
@ -209,10 +218,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.statusReplyingTo = this.statusReplyingToWrapper.status;
}
let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
if (state && state !== '') {
this.status = state;
} else {
// let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
// if (state && state !== '') {
// this.status = state;
// } else {
if(!this.status || this.status === '') {
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
for (const mention of uniqueMentions) {
this.status += `@${mention} `;
@ -232,7 +242,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.innerHeight = window.innerHeight;
}
ngOnDestroy() {
ngOnDestroy() {
if(this.isRedrafting){
this.statusStateService.resetStatusContent(null);
}
this.accountSub.unsubscribe();
}
@ -563,7 +577,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.scheduledStatusService.statusAdded(acc);
}
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
if(this.isRedrafting){
this.statusStateService.resetStatusContent(null);
} else {
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, acc);
@ -741,9 +759,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
if (isVisible) {
setTimeout(() => {
try{
try {
this.footerElement.nativeElement.scrollIntoViewIfNeeded({ behavior: 'instant', block: 'end', inline: 'start' });
}catch(err) {
} catch (err) {
this.footerElement.nativeElement.scrollIntoView({ behavior: 'instant', block: 'end', inline: 'start' });
}
}, 0);
@ -850,11 +868,11 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
const mastodonMentionRegex = /<span class="h-card"><a href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" class="u-url mention">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
const pleromaMentionRegex = /<span class="h-card"><a data-user="[a-zA-Z0-9]{0,255}" class="u-url mention" href="https:\/\/([a-zA-Z0-9.]{0,255})\/[a-zA-Z0-9_@/-]{0,255}" rel="ugc">@<span>([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi;
while(data.match(mastodonMentionRegex)){
while (data.match(mastodonMentionRegex)) {
data = data.replace(mastodonMentionRegex, '@$2@$1');
}
while(data.match(pleromaMentionRegex)){
while (data.match(pleromaMentionRegex)) {
data = data.replace(pleromaMentionRegex, '@$2@$1');
}

View File

@ -0,0 +1,5 @@
@import "variables";
.stream-toots {
background-color: $column-background;
}

View File

@ -0,0 +1,5 @@
@import "variables";
.stream-toots {
background-color: $column-background;
}

View File

@ -1,7 +1,8 @@
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener } from '@angular/core';
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener, OnDestroy } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';
import { faTimes, faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { Subject } from 'rxjs';
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { OpenMediaEvent } from '../../models/common.model';
import { Attachment, PleromaAttachment } from '../../services/models/mastodon.interfaces';
@ -12,7 +13,7 @@ import { Attachment, PleromaAttachment } from '../../services/models/mastodon.in
templateUrl: './media-viewer.component.html',
styleUrls: ['./media-viewer.component.scss']
})
export class MediaViewerComponent implements OnInit {
export class MediaViewerComponent implements OnInit, OnDestroy {
private _mediaEvent: OpenMediaEvent;
faTimes = faTimes;
faAngleLeft = faAngleLeft;
@ -64,9 +65,20 @@ export class MediaViewerComponent implements OnInit {
}
}
constructor() { }
private escapeHotkey = new Hotkey('escape', (event: KeyboardEvent): boolean => {
console.warn('CLOSE');
this.close();
return false;
});
constructor(private readonly hotkeysService: HotkeysService) { }
ngOnInit() {
this.hotkeysService.add(this.escapeHotkey);
}
ngOnDestroy(): void {
this.hotkeysService.remove(this.escapeHotkey);
}
private setBrowsing() {

View File

@ -169,4 +169,11 @@ describe('DatabindedTextComponent', () => {
component.text = sample;
expect(component.processedText).toContain('Bla<br /><br /><a href class="link-httpslink" title="open link">https://link/</a>');
});
it('should sanitize link', () => {
const sample = `https://domain.fr/public.php?op=rss&amp;id=-2&amp;key=60c63a21c2928546b4485017876fe850c6ebcebd#tag:domain.fr,2020-05-26:/49902061`;
let result = (<any>component).sanitizeLink(sample);
expect(result).toBe('https://domain.fr/public.php?op=rss&id=-2&key=60c63a21c2928546b4485017876fe850c6ebcebd#tag:domain.fr,2020-05-26:/49902061');
});
});

View File

@ -210,12 +210,14 @@ export class DatabindedTextComponent implements OnInit {
let classname = this.getClassNameForLink(link);
let els = this.contentElement.nativeElement.querySelectorAll(`.${classname}`);
let sanitizedLink = this.sanitizeLink(link);
for (const el of els) {
this.renderer.listen(el, 'click', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
window.open(link, '_blank');
window.open(sanitizedLink, '_blank');
return false;
});
@ -224,7 +226,7 @@ export class DatabindedTextComponent implements OnInit {
event.preventDefault();
event.stopImmediatePropagation();
window.open(link, '_blank');
window.open(sanitizedLink, '_blank');
return false;
}
});
@ -232,6 +234,11 @@ export class DatabindedTextComponent implements OnInit {
}
}
private sanitizeLink(link: string): string {
let res = link.replace(/&amp;/g, '&');
return res;
}
private getClassNameForHastag(value: string): string {
let res = value.replace(/[.,\/#?!@$%+\^&\*;:{}=\-_`~()]/g, "");
return `hashtag-${res}`;

View File

@ -107,13 +107,13 @@ export class UserProfileComponent implements OnInit {
this.relationShipError = false;
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
if(!account) throw Error(`Could not find ${this.lastAccountName}`);
if (!account) throw Error(`Could not find ${this.lastAccountName}`);
return this.getFollowStatus(userAccount, account);
})
.catch((err) => {
console.error(err);
this.relationShipError = true;
this.relationShipError = true;
})
.then(() => {
this.loadingRelationShip = false;
@ -128,7 +128,7 @@ export class UserProfileComponent implements OnInit {
});
}
});
}
}
ngOnDestroy() {
if (this.accountSub) this.accountSub.unsubscribe();
@ -164,7 +164,7 @@ export class UserProfileComponent implements OnInit {
this.isLoading = false;
this.statusLoading = true;
if(!account) throw Error(`Could not find ${this.lastAccountName}`);
if (!account) throw Error(`Could not find ${this.lastAccountName}`);
this.displayedAccount = this.fixPleromaFieldsUrl(account);
this.hasNote = account && account.note && account.note !== '<p></p>';
@ -188,9 +188,9 @@ export class UserProfileComponent implements OnInit {
}
private fixPleromaFieldsUrl(acc: Account): Account {
if(acc.fields){
if (acc.fields) {
acc.fields.forEach(f => {
if(f.value.includes('<a href="') && !f.value.includes('target="_blank"')){
if (f.value.includes('<a href="') && !f.value.includes('target="_blank"')) {
f.value = f.value.replace('<a href="', '<a target="_blank" href="');
}
});
@ -271,7 +271,7 @@ export class UserProfileComponent implements OnInit {
}
browseAccount(accountName: string): void {
if(accountName === this.toolsService.getAccountFullHandle(this.displayedAccount)) return;
if (accountName === this.toolsService.getAccountFullHandle(this.displayedAccount)) return;
this.browseAccountEvent.next(accountName);
}
@ -330,11 +330,13 @@ export class UserProfileComponent implements OnInit {
this.showFloatingHeader = false;
}
const menuPosition = element.scrollHeight - this.profilestatuses.nativeElement.offsetHeight - 30 - 31;
if (element.scrollTop > menuPosition) {
this.showFloatingStatusMenu = true;
} else {
this.showFloatingStatusMenu = false;
if (this.profilestatuses) {
const menuPosition = element.scrollHeight - this.profilestatuses.nativeElement.offsetHeight - 30 - 31;
if (element.scrollTop > menuPosition) {
this.showFloatingStatusMenu = true;
} else {
this.showFloatingStatusMenu = false;
}
}
if (atBottom) {
@ -343,7 +345,7 @@ export class UserProfileComponent implements OnInit {
}
private scrolledToBottom() {
if (this.statusLoading || this.maxReached) return;
if (this.statusLoading || this.maxReached || !this.displayedAccount) return;
const onlyMedia = this.statusSection === 'media';
const excludeReplies = this.statusSection === 'status';