commit
9eb6fc61ae
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sengi",
|
||||
"version": "0.26.1",
|
||||
"version": "0.27.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
|
|
@ -195,4 +195,32 @@ describe('CreateStatusComponent', () => {
|
|||
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1]).toBe('http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - mastodon', () => {
|
||||
let mastodonMention = '<p>test <span class="h-card"><a href="https://mastodon.social/@sengi_app" class="u-url mention">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a href="https://mastodon.social/@test" class="u-url mention">@<span>test</span></a></span> <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(mastodonMention);
|
||||
expect(result).toBe('<p>test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - mastodon 2', () => {
|
||||
let mastodonMention = '<p>test <span class="h-card"><a href="https://pleroma.site/users/sengi_app" class="u-url mention">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a href="https://pleroma.site/users/test" class="u-url mention">@<span>test</span></a></span> <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(mastodonMention);
|
||||
expect(result).toBe('<p>test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - pleroma', () => {
|
||||
let pleromaMention = '<p>test <span class="h-card"><a data-user="50504" class="u-url mention" href="https://mastodon.social/@sengi_app" rel="ugc">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a data-user="50504" class="u-url mention" href="https://mastodon.social/@test" rel="ugc">@<span>test</span></a></span> <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(pleromaMention);
|
||||
expect(result).toBe('<p>test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social <span class="h-card"><a href="https://mastodon.social/@no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
|
||||
it('should tranform external mentions properly - pleroma 2', () => {
|
||||
let pleromaMention = '<p>test <span class="h-card"><a data-user="50504" class="u-url mention" href="https://pleroma.site/users/sengi_app" rel="ugc">@<span>sengi_app</span></a></span> qsdqds qsd qsd qsd q <span class="h-card"><a data-user="50504" class="u-url mention" href="https://pleroma.site/users/test" rel="ugc">@<span>test</span></a></span> <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>';
|
||||
|
||||
const result = <string>(<any>component).tranformHtmlRepliesToReplies(pleromaMention);
|
||||
expect(result).toBe('<p>test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site <span class="h-card"><a href="https://pleroma.site/users/no" class="u-url">@<span>no</span></a></span></p>');
|
||||
});
|
||||
});
|
|
@ -1,5 +1,7 @@
|
|||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild, ViewContainerRef, ComponentRef, HostListener } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { UP_ARROW, DOWN_ARROW, ENTER, ESCAPE } from '@angular/cdk/keycodes';
|
||||
|
@ -17,12 +19,11 @@ import { AccountInfo } from '../../states/accounts.state';
|
|||
import { InstancesInfoService } from '../../services/instances-info.service';
|
||||
import { MediaService } from '../../services/media.service';
|
||||
import { AutosuggestSelection, AutosuggestUserActionEnum } from './autosuggest/autosuggest.component';
|
||||
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
|
||||
import { EmojiPickerComponent } from './emoji-picker/emoji-picker.component';
|
||||
import { PollEditorComponent } from './poll-editor/poll-editor.component';
|
||||
import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.component';
|
||||
import { ScheduledStatusService } from '../../services/scheduled-status.service';
|
||||
import { StatusesStateService } from '../../services/statuses-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-status',
|
||||
|
@ -53,6 +54,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private _status: string = '';
|
||||
@Input('status')
|
||||
set status(value: string) {
|
||||
this.statusStateService.setStatusContent(value, this.statusReplyingToWrapper);
|
||||
this.countStatusChar(value);
|
||||
this.detectAutosuggestion(value);
|
||||
this._status = value;
|
||||
|
@ -65,14 +67,38 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
return this._status;
|
||||
}
|
||||
|
||||
private trim(s, mask) {
|
||||
while (~mask.indexOf(s[0])) {
|
||||
s = s.slice(1);
|
||||
}
|
||||
while (~mask.indexOf(s[s.length - 1])) {
|
||||
s = s.slice(0, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@Input('redraftedStatus')
|
||||
set redraftedStatus(value: StatusWrapper) {
|
||||
if (value) {
|
||||
if (value) {
|
||||
this.statusLoaded = false;
|
||||
|
||||
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(value.status.content, 'text/html')
|
||||
var dom = parser.parseFromString(content, 'text/html')
|
||||
this.status = dom.body.textContent;
|
||||
|
||||
this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
|
||||
|
||||
this.setVisibilityFromStatus(value.status);
|
||||
this.title = value.status.spoiler_text;
|
||||
this.statusLoaded = true;
|
||||
|
@ -83,17 +109,6 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
.then((status: Status) => {
|
||||
let cwResult = this.toolsService.checkContentWarning(status);
|
||||
this.statusReplyingToWrapper = new StatusWrapper(cwResult.status, value.provider, cwResult.applyCw, cwResult.hide);
|
||||
|
||||
const mentions = this.getMentions(this.statusReplyingToWrapper.status, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of mentions) {
|
||||
const name = `@${mention.split('@')[0]}`;
|
||||
if (this.status.includes(name)) {
|
||||
this.status = this.status.replace(name, `@${mention}`);
|
||||
} else {
|
||||
this.status = `@${mention} ` + this.status;
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
this.notificationService.notifyHttpError(err, value.provider);
|
||||
|
@ -159,6 +174,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private selectedAccount: AccountInfo;
|
||||
|
||||
constructor(
|
||||
private statusStateService: StatusesStateService,
|
||||
private readonly scheduledStatusService: ScheduledStatusService,
|
||||
private readonly contextMenuService: ContextMenuService,
|
||||
private readonly store: Store,
|
||||
|
@ -169,7 +185,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
private readonly mediaService: MediaService,
|
||||
private readonly overlay: Overlay,
|
||||
public viewContainerRef: ViewContainerRef) {
|
||||
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -185,9 +203,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.statusReplyingTo = this.statusReplyingToWrapper.status;
|
||||
}
|
||||
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
let state = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
|
||||
if (state && state !== '') {
|
||||
this.status = state;
|
||||
} else {
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
}
|
||||
|
||||
this.setVisibilityFromStatus(this.statusReplyingTo);
|
||||
|
@ -533,6 +556,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
if (this.scheduleIsActive) {
|
||||
this.scheduledStatusService.statusAdded(acc);
|
||||
}
|
||||
|
||||
this.statusStateService.resetStatusContent(this.statusReplyingToWrapper);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err, acc);
|
||||
|
@ -810,4 +835,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
|||
this.scheduleIsActive = !this.scheduleIsActive;
|
||||
return false;
|
||||
}
|
||||
|
||||
private tranformHtmlRepliesToReplies(data: string): string {
|
||||
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)){
|
||||
data = data.replace(mastodonMentionRegex, '@$2@$1');
|
||||
}
|
||||
|
||||
while(data.match(pleromaMentionRegex)){
|
||||
data = data.replace(pleromaMentionRegex, '@$2@$1');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,11 +74,11 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.processNewMentions(userNotifications);
|
||||
if(this.statuses.length < 20) this.scrolledToBottom();
|
||||
if (this.statuses.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
private processNewMentions(userNotifications: UserNotification[]) {
|
||||
private processNewMentions(userNotifications: UserNotification[]) {
|
||||
const userNotification = userNotifications.find(x => x.account.id === this.account.info.id);
|
||||
if (userNotification && userNotification.mentions) {
|
||||
let orderedMentions = [...userNotification.mentions.map(x => x.status)].reverse();
|
||||
|
@ -120,7 +120,9 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
|||
for (const s of statuses) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s);
|
||||
const wrapper = new StatusWrapper(cwPolicy.status, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.statuses.push(wrapper);
|
||||
if (!this.statuses.find(x => x.status.id === s.id)) {
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastId = result[result.length - 1].id;
|
||||
|
|
|
@ -31,7 +31,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
get account(): AccountWrapper {
|
||||
return this._account;
|
||||
}
|
||||
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
|
||||
private maxReached = false;
|
||||
|
@ -39,7 +39,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
private userNotificationServiceSub: Subscription;
|
||||
private lastId: string;
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly userNotificationService: UserNotificationService,
|
||||
|
@ -49,22 +49,22 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if(this.userNotificationServiceSub){
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private loadNotifications(){
|
||||
if(this.userNotificationServiceSub){
|
||||
private loadNotifications() {
|
||||
if (this.userNotificationServiceSub) {
|
||||
this.userNotificationServiceSub.unsubscribe();
|
||||
}
|
||||
|
||||
this.notifications.length = 0;
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
|
||||
this.userNotificationServiceSub = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
|
||||
this.processNewNotifications(userNotifications);
|
||||
if(this.notifications.length < 20) this.scrolledToBottom();
|
||||
if (this.notifications.length < 20) this.scrolledToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
for (let n of orderedNotifications) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(n.status);
|
||||
const notificationWrapper = new NotificationWrapper(n, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
|
||||
if (!this.notifications.find(x => x.wrapperId === notificationWrapper.wrapperId)) {
|
||||
if (!this.notifications.find(x => x.wrapperId === notificationWrapper.wrapperId)) {
|
||||
this.notifications.unshift(notificationWrapper);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
this.userNotificationService.markNotificationAsRead(this.account.info);
|
||||
}
|
||||
|
||||
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
|
@ -105,11 +105,13 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
this.maxReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const s of notifications) {
|
||||
let cwPolicy = this.toolsService.checkContentWarning(s.status);
|
||||
const wrapper = new NotificationWrapper(s, this.account.info, cwPolicy.applyCw, cwPolicy.hide);
|
||||
this.notifications.push(wrapper);
|
||||
if (!this.notifications.find(x => x.wrapperId === wrapper.wrapperId)) {
|
||||
this.notifications.push(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastId = notifications[notifications.length - 1].id;
|
||||
|
@ -136,16 +138,16 @@ export class NotificationsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
export class NotificationWrapper {
|
||||
constructor(notification: Notification, provider: AccountInfo, applyCw: boolean, hideStatus: boolean) {
|
||||
constructor(notification: Notification, provider: AccountInfo, applyCw: boolean, hideStatus: boolean) {
|
||||
this.type = notification.type;
|
||||
switch(this.type){
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
switch (this.type) {
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
case 'favourite':
|
||||
case 'poll':
|
||||
this.status= new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
||||
break;
|
||||
}
|
||||
case 'poll':
|
||||
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
|
||||
break;
|
||||
}
|
||||
this.account = notification.account;
|
||||
this.wrapperId = `${this.type}-${notification.id}`;
|
||||
this.notification = notification;
|
||||
|
|
|
@ -16,4 +16,6 @@
|
|||
Now <span class="underline">right-click</span> on your avatar to open your account and be able to add some timelines!
|
||||
</p>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
|
||||
<img class="sengi-logo" src="assets/icons/icon-384x384.png" />
|
|
@ -25,20 +25,20 @@
|
|||
|
||||
&__arrow {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
top: 20px;
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
&__title{
|
||||
position: relative;
|
||||
top: 30px;
|
||||
left: 70px;
|
||||
left: 85px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
position: relative;
|
||||
top: 45px;
|
||||
left: 75px;
|
||||
top: 60px;
|
||||
left: 70px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
|
@ -80,4 +80,11 @@
|
|||
// word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.sengi-logo {
|
||||
width: 230px;
|
||||
position: fixed;
|
||||
bottom: 35px;
|
||||
right: 10px;
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { StatusWrapper } from '../models/common.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StatusesStateService {
|
||||
private cachedStatusText: { [statusId: string]: string } = {};
|
||||
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
|
||||
public stateNotification = new Subject<StatusState>();
|
||||
|
||||
|
@ -62,6 +64,34 @@ export class StatusesStateService {
|
|||
|
||||
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{
|
||||
let data: string;
|
||||
if(replyingToStatus){
|
||||
data = this.cachedStatusText[replyingToStatus.status.uri];
|
||||
} else {
|
||||
data = this.cachedStatusText['none'];
|
||||
}
|
||||
|
||||
if(!data) return '';
|
||||
return data;
|
||||
}
|
||||
|
||||
resetStatusContent(replyingToStatus: StatusWrapper){
|
||||
if(replyingToStatus){
|
||||
this.cachedStatusText[replyingToStatus.status.uri] = '';
|
||||
} else {
|
||||
this.cachedStatusText['none'] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusState {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 10 KiB |
Loading…
Reference in New Issue