diff --git a/package.json b/package.json index ba7744c0..f2818c3b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/components/create-status/create-status.component.spec.ts b/src/app/components/create-status/create-status.component.spec.ts index 956d4efe..26a1c223 100644 --- a/src/app/components/create-status/create-status.component.spec.ts +++ b/src/app/components/create-status/create-status.component.spec.ts @@ -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 = '

test @sengi_app qsdqds qsd qsd qsd q @test @no

'; + + const result = (component).tranformHtmlRepliesToReplies(mastodonMention); + expect(result).toBe('

test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social @no

'); + }); + + it('should tranform external mentions properly - mastodon 2', () => { + let mastodonMention = '

test @sengi_app qsdqds qsd qsd qsd q @test @no

'; + + const result = (component).tranformHtmlRepliesToReplies(mastodonMention); + expect(result).toBe('

test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site @no

'); + }); + + it('should tranform external mentions properly - pleroma', () => { + let pleromaMention = '

test @sengi_app qsdqds qsd qsd qsd q @test @no

'; + + const result = (component).tranformHtmlRepliesToReplies(pleromaMention); + expect(result).toBe('

test @sengi_app@mastodon.social qsdqds qsd qsd qsd q @test@mastodon.social @no

'); + }); + + it('should tranform external mentions properly - pleroma 2', () => { + let pleromaMention = '

test @sengi_app qsdqds qsd qsd qsd q @test @no

'; + + const result = (component).tranformHtmlRepliesToReplies(pleromaMention); + expect(result).toBe('

test @sengi_app@pleroma.site qsdqds qsd qsd qsd q @test@pleroma.site @no

'); + }); }); \ No newline at end of file diff --git a/src/app/components/create-status/create-status.component.ts b/src/app/components/create-status/create-status.component.ts index 3287c6b8..a926199b 100644 --- a/src/app/components/create-status/create-status.component.ts +++ b/src/app/components/create-status/create-status.component.ts @@ -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('

') || content.includes('

') || content.includes('
') || content.includes('
') || content.includes('
')) { + content = content.replace('

', '').replace('

', newLine + newLine).replace('
', newLine).replace('
', newLine).replace('
', 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 = /@([a-zA-Z0-9_-]{0,255})<\/span><\/a><\/span>/gmi; + const pleromaMentionRegex = /@([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; + } } diff --git a/src/app/components/floating-column/manage-account/mentions/mentions.component.ts b/src/app/components/floating-column/manage-account/mentions/mentions.component.ts index 9e81b404..b90a56a2 100644 --- a/src/app/components/floating-column/manage-account/mentions/mentions.component.ts +++ b/src/app/components/floating-column/manage-account/mentions/mentions.component.ts @@ -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; diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.ts b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts index 03545b1f..35f4fe12 100644 --- a/src/app/components/floating-column/manage-account/notifications/notifications.component.ts +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts @@ -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; diff --git a/src/app/components/tutorial/tutorial.component.html b/src/app/components/tutorial/tutorial.component.html index 23077932..c6dadcf8 100644 --- a/src/app/components/tutorial/tutorial.component.html +++ b/src/app/components/tutorial/tutorial.component.html @@ -16,4 +16,6 @@ Now right-click on your avatar to open your account and be able to add some timelines!

- \ No newline at end of file + + + \ No newline at end of file diff --git a/src/app/components/tutorial/tutorial.component.scss b/src/app/components/tutorial/tutorial.component.scss index a5fcd38d..95766808 100644 --- a/src/app/components/tutorial/tutorial.component.scss +++ b/src/app/components/tutorial/tutorial.component.scss @@ -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; } \ No newline at end of file diff --git a/src/app/services/statuses-state.service.ts b/src/app/services/statuses-state.service.ts index 7982c7ec..68180bb7 100644 --- a/src/app/services/statuses-state.service.ts +++ b/src/app/services/statuses-state.service.ts @@ -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(); @@ -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 { diff --git a/src/assets/img/arrow_1.png b/src/assets/img/arrow_1.png index f74cd353..3e2615f7 100644 Binary files a/src/assets/img/arrow_1.png and b/src/assets/img/arrow_1.png differ diff --git a/src/assets/img/arrow_2.png b/src/assets/img/arrow_2.png index 6946ad86..f949e514 100644 Binary files a/src/assets/img/arrow_2.png and b/src/assets/img/arrow_2.png differ