Merge pull request #604 from NicolasConstant/develop

1.5.0 PR
This commit is contained in:
Nicolas Constant 2023-08-07 20:09:31 -04:00 committed by GitHub
commit c3cd6fe79e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1091 additions and 123 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "sengi", "name": "sengi",
"version": "1.2.0", "version": "1.4.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "sengi", "name": "sengi",
"version": "1.4.0", "version": "1.5.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"main": "main-electron.js", "main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma", "description": "A multi-account desktop client for Mastodon and Pleroma",
@ -20,8 +20,8 @@
"test": "ng test", "test": "ng test",
"test-nowatch": "ng test --watch=false", "test-nowatch": "ng test --watch=false",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "e2e": "ng e2e",
"dist": "npm run build" "dist": "npm run build"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

@ -5,8 +5,7 @@ import { HttpModule } from "@angular/http";
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
// import { NgxElectronModule } from 'ngx-electron';
// import { NgxElectronModule } from "ngx-electron";
import { NgxsModule } from '@ngxs/store'; import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
@ -90,6 +89,7 @@ import { TutorialEnhancedComponent } from './components/tutorial-enhanced/tutori
import { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component'; import { NotificationsTutorialComponent } from './components/tutorial-enhanced/notifications-tutorial/notifications-tutorial.component';
import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-tutorial.component'; import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-tutorial.component';
import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component'; import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component';
import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component';
const routes: Routes = [ const routes: Routes = [
{ path: "", component: StreamsMainDisplayComponent }, { path: "", component: StreamsMainDisplayComponent },
@ -159,7 +159,8 @@ const routes: Routes = [
TutorialEnhancedComponent, TutorialEnhancedComponent,
NotificationsTutorialComponent, NotificationsTutorialComponent,
LabelsTutorialComponent, LabelsTutorialComponent,
ThankyouTutorialComponent ThankyouTutorialComponent,
StatusTranslateComponent
], ],
entryComponents: [ entryComponents: [
EmojiPickerComponent EmojiPickerComponent
@ -176,6 +177,7 @@ const routes: Routes = [
OwlDateTimeModule, OwlDateTimeModule,
OwlNativeDateTimeModule, OwlNativeDateTimeModule,
OverlayModule, OverlayModule,
// NgxElectronModule,
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
NgxsModule.forRoot([ NgxsModule.forRoot([

View File

@ -1,16 +1,23 @@
<form class="status-editor" (ngSubmit)="onSubmit()"> <form class="status-editor" (ngSubmit)="onSubmit()">
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title" <input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" /> name="title" autocomplete="off" placeholder="Title, Content Warning (optional)"
title="title, content warning (optional)" dir="auto"
(keydown.escape)="mytitle.blur()" />
<a class="status-editor__emoji" title="Insert Emoji" <a class="status-editor__emoji" title="Insert Emoji"
#emojiButton href (click)="openEmojiPicker($event)"> #emojiButton href (click)="openEmojiPicker($event)">
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png"> <img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
</a> </a>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)" <a class="status-editor__lang" title="Change language" href *ngIf="configuredLanguages && configuredLanguages.length > 1" (click)="onLangContextMenu($event)">
{{ selectedLanguage.iso639 }}
</a>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
rows="5" required title="content" placeholder="What's on your mind?" rows="5" required title="content" placeholder="What's on your mind?"
(keydown.control.enter)="onCtrlEnter()" (keydown.control.enter)="onCtrlEnter()"
(keydown.meta.enter)="onCtrlEnter()" (keydown.meta.enter)="onCtrlEnter()"
(keydown.escape)="reply.blur()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto"> (keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
</textarea> </textarea>
@ -23,7 +30,7 @@
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)"> (suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
</app-autosuggest> </app-autosuggest>
<app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive"></app-poll-editor> <app-poll-editor *ngIf="instanceSupportsPoll && pollIsActive" [oldPoll]="oldPoll"></app-poll-editor>
<app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler> <app-status-scheduler class="scheduler" *ngIf="instanceSupportsScheduling && scheduleIsActive"></app-status-scheduler>
@ -68,6 +75,10 @@
<fa-icon [icon]="faClock"></fa-icon> <fa-icon [icon]="faClock"></fa-icon>
</a> </a>
</div> </div>
<div class="language-warning" *ngIf="!configuredLanguages || configuredLanguages.length === 0">
You haven't set your language(s) yet, please <a href class="language-warning__link" (click)="onNavigateToSettings()">go in the settings</a> to provide it.
</div>
<context-menu #contextMenu> <context-menu #contextMenu>
<ng-template contextMenuItem (execute)="changePrivacy('Public')"> <ng-template contextMenuItem (execute)="changePrivacy('Public')">
@ -83,5 +94,12 @@
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct <fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
</ng-template> </ng-template>
</context-menu> </context-menu>
<context-menu #langContextMenu>
<ng-template contextMenuItem (execute)="setLanguage(l)" *ngFor="let l of configuredLanguages">
{{ l.name }}
</ng-template>
</context-menu>
<app-media></app-media> <app-media></app-media>
</form> </form>

View File

@ -70,6 +70,32 @@ $counter-width: 90px;
} }
} }
&__lang {
position: absolute;
top: 64px;
right: 12px;
font-weight: bolder;
font-size: 12px;
color: #a5a5a5;
text-decoration: none;
display: block;
width: 20px;
height: 19px;
border-radius: 2px;
background-color: rgba(255, 255, 255, 0);
padding: 1px 0 0 2px;
text-transform: uppercase;
&:hover {
text-decoration: none;
color:black;
background-color: #e6e6e6;
}
}
&__content { &__content {
border-width: 0; border-width: 0;
background-color: $status-editor-background; background-color: $status-editor-background;
@ -207,6 +233,20 @@ $counter-width: 90px;
border-bottom: 1px solid whitesmoke; border-bottom: 1px solid whitesmoke;
} }
.language-warning {
padding: 5px 10px;
color: orange;
&__link {
text-decoration: underline;
color: #f0d124;
&:hover {
color: #d18800;
}
}
}
@import '~@angular/cdk/overlay-prebuilt.css'; @import '~@angular/cdk/overlay-prebuilt.css';
// ::ng-deep .cdk-overlay-backdrop { // ::ng-deep .cdk-overlay-backdrop {
// // width: 100%; // // width: 100%;

View File

@ -11,7 +11,7 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
import { VisibilityEnum, PollParameters } from '../../services/mastodon.service'; import { VisibilityEnum, PollParameters } from '../../services/mastodon.service';
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service'; import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
import { Status, Attachment } from '../../services/models/mastodon.interfaces'; import { Status, Attachment, Poll } from '../../services/models/mastodon.interfaces';
import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service'; import { ToolsService, InstanceInfo, InstanceType } from '../../services/tools.service';
import { NotificationService } from '../../services/notification.service'; import { NotificationService } from '../../services/notification.service';
import { StatusWrapper } from '../../models/common.model'; import { StatusWrapper } from '../../models/common.model';
@ -25,6 +25,9 @@ import { StatusSchedulerComponent } from './status-scheduler/status-scheduler.co
import { ScheduledStatusService } from '../../services/scheduled-status.service'; import { ScheduledStatusService } from '../../services/scheduled-status.service';
import { StatusesStateService } from '../../services/statuses-state.service'; import { StatusesStateService } from '../../services/statuses-state.service';
import { SettingsService } from '../../services/settings.service'; import { SettingsService } from '../../services/settings.service';
import { LanguageService } from '../../services/language.service';
import { ILanguage } from '../../states/settings.state';
import { LeftPanelType, NavigationService } from '../../services/navigation.service';
@Component({ @Component({
selector: 'app-create-status', selector: 'app-create-status',
@ -140,9 +143,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.isSending = false; this.isSending = false;
}); });
} }
if(value.status.poll){
this.pollIsActive = true;
this.oldPoll = value.status.poll;
// setTimeout(() => {
// if(this.pollEditor) this.pollEditor.loadPollParameters(value.status.poll);
// }, 250);
}
} }
} }
oldPoll: Poll;
private maxCharLength: number; private maxCharLength: number;
charCountLeft: number; charCountLeft: number;
postCounts: number = 1; postCounts: number = 1;
@ -153,6 +166,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
instanceSupportsScheduling = true; instanceSupportsScheduling = true;
isEditing: boolean; isEditing: boolean;
editingStatusId: string; editingStatusId: string;
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
private statusLoaded: boolean; private statusLoaded: boolean;
private hasSuggestions: boolean; private hasSuggestions: boolean;
@ -162,6 +177,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
@ViewChild('fileInput') fileInputElement: ElementRef; @ViewChild('fileInput') fileInputElement: ElementRef;
@ViewChild('footer') footerElement: ElementRef; @ViewChild('footer') footerElement: ElementRef;
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent; @ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent; @ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent; @ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
@ -196,11 +212,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private accounts$: Observable<AccountInfo[]>; private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription; private accountSub: Subscription;
private langSub: Subscription;
private selectLangSub: Subscription;
private selectedAccount: AccountInfo; private selectedAccount: AccountInfo;
constructor( constructor(
private readonly navigationService: NavigationService,
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService, private readonly settingsService: SettingsService,
private statusStateService: StatusesStateService, private readonly statusStateService: StatusesStateService,
private readonly scheduledStatusService: ScheduledStatusService, private readonly scheduledStatusService: ScheduledStatusService,
private readonly contextMenuService: ContextMenuService, private readonly contextMenuService: ContextMenuService,
private readonly store: Store, private readonly store: Store,
@ -216,7 +236,35 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts); this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
} }
private initLanguages(){
this.configuredLanguages = this.languageService.getConfiguredLanguages();
this.selectedLanguage = this.languageService.getSelectedLanguage();
this.langSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
this.configuredLanguages = l;
// if(this.configuredLanguages.length > 0
// && this.selectedLanguage
// && this.configuredLanguages.findIndex(x => x.iso639 === this.selectedLanguage.iso639)){
// this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
// }
});
this.selectLangSub = this.languageService.selectedLanguageChanged.subscribe(l => {
this.selectedLanguage = l;
});
if(!this.selectedLanguage && this.configuredLanguages.length > 0){
this.languageService.setSelectedLanguage(this.configuredLanguages[0]);
}
}
setLanguage(lang: ILanguage): boolean {
if(lang){
this.languageService.setSelectedLanguage(lang);
}
return false;
}
ngOnInit() { ngOnInit() {
this.initLanguages();
if (!this.isRedrafting) { if (!this.isRedrafting) {
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper); this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
} }
@ -263,6 +311,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
} }
this.accountSub.unsubscribe(); this.accountSub.unsubscribe();
this.langSub.unsubscribe();
this.selectLangSub.unsubscribe();
}
onNavigateToSettings(): boolean {
this.navigationService.openPanel(LeftPanelType.Settings);
return false;
} }
onPaste(e: any) { onPaste(e: any) {
@ -613,6 +668,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return false; return false;
} }
private currentLang(): string {
if(this.selectedLanguage){
return this.selectedLanguage.iso639;
}
return null;
}
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[], poll: PollParameters, scheduledAt: string, editingStatusId: 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 parsedStatus = this.parseStatus(status);
let resultPromise = Promise.resolve(previousStatus); let resultPromise = Promise.resolve(previousStatus);
@ -630,9 +693,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
let postPromise: Promise<Status>; let postPromise: Promise<Status>;
if (this.isEditing) { if (this.isEditing) {
postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments, poll, scheduledAt); postPromise = this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, attachments, poll, scheduledAt, this.currentLang());
} else { } else {
postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt); postPromise = this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id), poll, scheduledAt, this.currentLang());
} }
return postPromise return postPromise
@ -642,9 +705,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}); });
} else { } else {
if (this.isEditing) { if (this.isEditing) {
return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt); return this.mastodonService.editStatus(account, editingStatusId, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
} else { } else {
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt); return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, [], null, scheduledAt, this.currentLang());
} }
} }
}) })
@ -887,6 +950,17 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
$event.stopPropagation(); $event.stopPropagation();
} }
public onLangContextMenu($event: MouseEvent): void {
this.contextMenuService.show.next({
// Optional - if unspecified, all context menu components will open
contextMenu: this.langContextMenu,
event: $event,
item: null
});
$event.preventDefault();
$event.stopPropagation();
}
//https://stackblitz.com/edit/overlay-demo //https://stackblitz.com/edit/overlay-demo
@ViewChild('emojiButton') emojiButtonElement: ElementRef; @ViewChild('emojiButton') emojiButtonElement: ElementRef;
overlayRef: OverlayRef; overlayRef: OverlayRef;

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, Input, OnInit, SimpleChanges } from '@angular/core';
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { PollEntry } from './poll-entry/poll-entry.component'; import { PollEntry } from './poll-entry/poll-entry.component';
import { PollParameters } from '../../../services/mastodon.service'; import { PollParameters } from '../../../services/mastodon.service';
import { retry } from 'rxjs/operators'; import { Poll } from '../../../services/models/mastodon.interfaces';
@Component({ @Component({
selector: 'app-poll-editor', selector: 'app-poll-editor',
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
selectedId: string; selectedId: string;
private multiSelected: boolean; private multiSelected: boolean;
@Input() oldPoll: Poll;
constructor() { constructor() {
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected)); this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected)); this.entries.push(new PollEntry(this.getEntryUuid(), this.multiSelected));
@ -40,6 +42,12 @@ export class PollEditorComponent implements OnInit {
} }
ngOnChanges(changes: SimpleChanges): void {
if (changes['oldPoll']) {
this.loadPollParameters(this.oldPoll);
}
}
private getEntryUuid(): number { private getEntryUuid(): number {
this.entryUuid++; this.entryUuid++;
return this.entryUuid; return this.entryUuid;
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
return false; return false;
} }
removeElement(entry: PollEntry){ removeElement(entry: PollEntry) {
this.entries = this.entries.filter(x => x.id != entry.id); this.entries = this.entries.filter(x => x.id != entry.id);
} }
@ -69,6 +77,17 @@ export class PollEditorComponent implements OnInit {
params.hide_totals = false; params.hide_totals = false;
return params; return params;
} }
private loadPollParameters(poll: Poll) {
const isMulti = poll.multiple;
this.entries.length = 0;
for (let o of poll.options) {
const entry = new PollEntry(this.getEntryUuid(), isMulti);
entry.label = o.title;
this.entries.push(entry);
}
}
} }
class Delay { class Delay {

View File

@ -60,8 +60,8 @@ export class ManageAccountComponent extends BrowseBase {
private readonly mastodonService: MastodonWrapperService, private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly userNotificationService: UserNotificationService) { private readonly userNotificationService: UserNotificationService) {
super(); super();
} }
ngOnInit() { ngOnInit() {
} }
@ -71,13 +71,9 @@ export class ManageAccountComponent extends BrowseBase {
} }
private checkIfBookmarksAreAvailable() { private checkIfBookmarksAreAvailable() {
this.toolsService.getInstanceInfo(this.account.info) this.toolsService.isBookmarksAreAvailable(this.account.info)
.then((instance: InstanceInfo) => { .then((isAvailable: boolean) => {
if (instance.major == 3 && instance.minor >= 1 || instance.major > 3) { this.isBookmarksAvailable = isAvailable;
this.isBookmarksAvailable = true;
} else {
this.isBookmarksAvailable = false;
}
}) })
.catch(err => { .catch(err => {
this.isBookmarksAvailable = false; this.isBookmarksAvailable = false;
@ -128,15 +124,15 @@ export class ManageAccountComponent extends BrowseBase {
} }
} }
@ViewChild('bookmarks') bookmarksComp:BookmarksComponent; @ViewChild('bookmarks') bookmarksComp: BookmarksComponent;
@ViewChild('notifications') notificationsComp:NotificationsComponent; @ViewChild('notifications') notificationsComp: NotificationsComponent;
@ViewChild('mentions') mentionsComp:MentionsComponent; @ViewChild('mentions') mentionsComp: MentionsComponent;
@ViewChild('dm') dmComp:DirectMessagesComponent; @ViewChild('dm') dmComp: DirectMessagesComponent;
@ViewChild('favorites') favoritesComp:FavoritesComponent; @ViewChild('favorites') favoritesComp: FavoritesComponent;
loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean { loadSubPanel(subpanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks'): boolean {
if(this.subPanel === subpanel){ if (this.subPanel === subpanel) {
switch(subpanel){ switch (subpanel) {
case 'bookmarks': case 'bookmarks':
this.bookmarksComp.applyGoToTop(); this.bookmarksComp.applyGoToTop();
break; break;

View File

@ -22,7 +22,7 @@
(click)="acceptFollowRequest()"> (click)="acceptFollowRequest()">
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon> <fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
</a> </a>
<a href title="Reject" class="follow_request__link follow_request__link--cross" <a href title="Reject" class="follow_request__link follow_request__link--cross"
(click)="refuseFollowRequest()"> (click)="refuseFollowRequest()">
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon> <fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
</a> </a>
@ -69,12 +69,18 @@
</a> </a>
</div> </div>
<app-status *ngIf="notification.status && notification.type !== 'mention'" class="stream__status" [statusWrapper]="notification.status" <app-status *ngIf="notification.status && notification.type === 'update'" class="stream__status"
[notificationAccount]="notification.account" [notificationType]="notification.type" [statusWrapper]="notification.status" [notificationAccount]="notification.account"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)" [notificationType]="notification.type" (browseAccountEvent)="browseAccount($event)"
(browseThreadEvent)="browseThread($event)"></app-status> (browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-status>
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status"
[statusWrapper]="notification.status" (browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-status>
<app-status *ngIf="notification.status && notification.type !== 'mention' && notification.type !== 'update'"
class="stream__status" [statusWrapper]="notification.status" [notificationAccount]="notification.account"
[notificationType]="notification.type" (browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-status>
<app-status *ngIf="notification.status && notification.type === 'mention'" class="stream__status" [statusWrapper]="notification.status"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div> </div>

View File

@ -101,7 +101,7 @@ export class NotificationsComponent extends BrowseBase {
this.isLoading = true; this.isLoading = true;
this.isProcessingInfiniteScroll = true; this.isProcessingInfiniteScroll = true;
this.mastodonService.getNotifications(this.account.info, ['mention', 'update'], this.lastId) this.mastodonService.getNotifications(this.account.info, ['mention'], this.lastId)
.then((notifications: Notification[]) => { .then((notifications: Notification[]) => {
if (notifications.length === 0) { if (notifications.length === 0) {
this.maxReached = true; this.maxReached = true;
@ -152,6 +152,7 @@ export class NotificationWrapper {
case 'reblog': case 'reblog':
case 'favourite': case 'favourite':
case 'poll': case 'poll':
case 'update':
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus); this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
break; break;
} }

View File

@ -4,8 +4,8 @@
<h3 class="panel__title">search</h3> <h3 class="panel__title">search</h3>
<form class="form-section" (ngSubmit)="onSubmit()"> <form class="form-section" (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle" <input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" /> name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
<button class="form-button" type="submit" title="search">GO</button> <button class="form-button" type="submit" title="search">GO</button>
</form> </form>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service'; import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild('search') searchElement: ElementRef;
constructor( constructor(
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) { } private readonly mastodonService: MastodonWrapperService) { }
ngOnInit() { ngOnInit() {
this.searchElement.nativeElement.focus();
} }
onSubmit(): boolean { onSubmit(): boolean {

View File

@ -35,7 +35,7 @@
</form> </form>
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a> <a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
</div> </div>
<h4 class="panel__subtitle">Shortcuts</h4> <h4 class="panel__subtitle">Shortcuts</h4>
<div class="sub-section"> <div class="sub-section">
<span class="sub-section__title">switch column:</span><br /> <span class="sub-section__title">switch column:</span><br />
@ -51,21 +51,42 @@
<br> <br>
</div> </div>
<h4 class="panel__subtitle">Languages</h4>
<div class="sub-section">
<div class="sub-section__content">
<div *ngIf="!configuredLangs || configuredLangs.length === 0" class="language__warning">
No language set.
</div>
<div *ngFor="let l of configuredLangs" class="language__entry">
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
<a href (click)="onRemoveLang(l)" class="form-button language__entry__action sound__play">remove</a>
</div>
<input type="text" (input)="onSearchLang($event.target.value)" [(ngModel)]="searchLang"
placeholder="Find Language" autocomplete="off" class="form-control form-control-sm language__search"/>
<div *ngFor="let l of searchedLangs" class="language__entry">
<span class="language__entry__name">{{ l.name }} ({{l.iso639}})</span>
<a href (click)="onAddLang(l)" class="form-button language__entry__action sound__play">add</a>
</div>
</div>
</div>
<h4 class="panel__subtitle">Twitter Bridge</h4> <h4 class="panel__subtitle">Twitter Bridge</h4>
<div class="sub-section"> <div class="sub-section">
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled" <input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled" (change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled"> value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label> <label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
<br> <br>
<div *ngIf="twitterBridgeEnabled"> <div *ngIf="twitterBridgeEnabled">
<p>Please provide your bridge instance: <p>Please provide your bridge instance:
<input type="text" class="form-control form-control-sm sub_section__text-input" <input type="text" class="form-control form-control-sm sub_section__text-input"
[(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" /> [(ngModel)]="setTwitterBridgeInstance" placeholder="bridge.tld" />
If you don't know any, consider using <a href="https://github.com/NicolasConstant/BirdsiteLive" target="_blank" class="version__link">BirdsiteLIVE</a></p> If you don't know any, consider using <a href="https://github.com/NicolasConstant/BirdsiteLive"
target="_blank" class="version__link">BirdsiteLIVE</a>
</p>
</div> </div>
<div> <div>
<a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank" class="version__link">What is this?</a> <a href="https://github.com/NicolasConstant/sengi/wiki/BirdsiteLIVE-integration" target="_blank"
class="version__link">What is this?</a>
</div> </div>
</div> </div>
@ -79,7 +100,7 @@
<input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)" <input class="sub-section__checkbox" [checked]="contentWarningPolicy === 2" (change)="onCwPolicyChange(2)"
type="radio" name="cw-hide-all" value="cw-hide-all" id="cw-hide-all"> type="radio" name="cw-hide-all" value="cw-hide-all" id="cw-hide-all">
<label class="noselect sub-section__label" for="cw-hide-all">Hide all CWs</label> <label class="noselect sub-section__label" for="cw-hide-all">Expand all CWs</label>
<br> <br>
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2"> <div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
<span class="sub-section__title">but add CW on content containing:</span><br /> <span class="sub-section__title">but add CW on content containing:</span><br />

View File

@ -31,6 +31,13 @@
padding: 0 5px 15px 5px; padding: 0 5px 15px 5px;
position: relative; position: relative;
&__content {
display: block;
padding: 0 0 0 5px;
// outline: 1px dotted greenyellow;
}
&__checkbox { &__checkbox {
position: relative; position: relative;
top: 3px; top: 3px;
@ -68,6 +75,41 @@
} }
} }
.language {
&__warning {
color: orange;
}
&__entry {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
&:not(:last-child){
margin-bottom: 1px;
}
&__name {
display: block;
align-items: stretch;
padding-left: 5px;
}
&__action {
align-items: stretch;
min-width: 70px;
text-align: center;
padding: 0 10px;
}
}
&__search {
display: block;
margin: 5px 0 5px 0;
}
}
.form-control { .form-control {
border: 1px solid $settings-text-input-border; border: 1px solid $settings-text-input-border;
color: $settings-text-input-foreground; color: $settings-text-input-foreground;

View File

@ -1,15 +1,17 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Howl } from 'howler'; import { Howl } from 'howler';
import { Subscription } from 'rxjs';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ToolsService, InstanceType } from '../../../services/tools.service'; import { ToolsService, InstanceType } from '../../../services/tools.service';
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service'; import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
import { ServiceWorkerService } from '../../../services/service-worker.service'; import { ServiceWorkerService } from '../../../services/service-worker.service';
import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum } from '../../../states/settings.state'; import { ContentWarningPolicy, ContentWarningPolicyEnum, TimeLineModeEnum, TimeLineHeaderEnum, ILanguage } from '../../../states/settings.state';
import { NotificationService } from '../../../services/notification.service'; import { NotificationService } from '../../../services/notification.service';
import { NavigationService } from '../../../services/navigation.service'; import { NavigationService } from '../../../services/navigation.service';
import { SettingsService } from '../../../services/settings.service'; import { SettingsService } from '../../../services/settings.service';
import { LanguageService } from '../../../services/language.service';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
@ -17,7 +19,7 @@ import { SettingsService } from '../../../services/settings.service';
styleUrls: ['./settings.component.scss'] styleUrls: ['./settings.component.scss']
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit, OnDestroy {
notificationSounds: NotificationSoundDefinition[]; notificationSounds: NotificationSoundDefinition[];
notificationSoundId: string; notificationSoundId: string;
@ -39,6 +41,10 @@ export class SettingsComponent implements OnInit {
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop; timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None; contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
configuredLangs: ILanguage[] = [];
searchedLangs: ILanguage[] = [];
searchLang: string;
private addCwOnContent: string; private addCwOnContent: string;
set setAddCwOnContent(value: string) { set setAddCwOnContent(value: string) {
this.setCwPolicy(null, value, null, null); this.setCwPolicy(null, value, null, null);
@ -76,16 +82,25 @@ export class SettingsComponent implements OnInit {
return this.twitterBridgeInstance; return this.twitterBridgeInstance;
} }
private languageSub: Subscription;
constructor( constructor(
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService, private readonly settingsService: SettingsService,
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private serviceWorkersService: ServiceWorkerService, private serviceWorkersService: ServiceWorkerService,
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly userNotificationsService: UserNotificationService) { } private readonly userNotificationsService: UserNotificationService) { }
ngOnInit() { ngOnInit() {
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
if(l){
this.configuredLangs = l;
}
});
this.version = environment.VERSION; this.version = environment.VERSION;
const settings = this.settingsService.getSettings(); const settings = this.settingsService.getSettings();
@ -129,6 +144,34 @@ export class SettingsComponent implements OnInit {
this.twitterBridgeEnabled = settings.twitterBridgeEnabled; this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
this.twitterBridgeInstance = settings.twitterBridgeInstance; this.twitterBridgeInstance = settings.twitterBridgeInstance;
this.configuredLangs = this.languageService.getConfiguredLanguages();
}
ngOnDestroy(): void {
if(this.languageSub) this.languageSub.unsubscribe();
}
onSearchLang(input: string) {
this.searchedLangs = this.languageService.searchLanguage(input);
}
onAddLang(lang: ILanguage): boolean {
if(this.configuredLangs.findIndex(x => x.iso639 === lang.iso639) >= 0) return false;
// this.configuredLangs.push(lang);
this.languageService.addLanguage(lang);
this.searchLang = '';
this.searchedLangs.length = 0;
return false;
}
onRemoveLang(lang: ILanguage): boolean {
// this.configuredLangs = this.configuredLangs.filter(x => x.iso639 !== lang.iso639);
this.languageService.removeLanguage(lang);
return false;
} }
onShortcutChange(id: ColumnShortcut) { onShortcutChange(id: ColumnShortcut) {

View File

@ -43,7 +43,7 @@ $inner-column-size: 320px;
&__follow-button { &__follow-button {
position: absolute; position: absolute;
top: 7px; top: 7px;
right: 100px; right: 114px;
padding: 0 10px 0 10px; padding: 0 10px 0 10px;
border: 1px solid black; border: 1px solid black;
color: white; color: white;

View File

@ -342,13 +342,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
} }
private checkIfBookmarksAreAvailable(account: AccountInfo) { private checkIfBookmarksAreAvailable(account: AccountInfo) {
this.toolsService.getInstanceInfo(account) this.toolsService.isBookmarksAreAvailable(account)
.then((instance: InstanceInfo) => { .then((isAvailable: boolean) => {
if (instance.major == 3 && instance.minor >= 1 || instance.major > 3) { this.isBookmarksAvailable = isAvailable;
this.isBookmarksAvailable = true;
} else {
this.isBookmarksAvailable = false;
}
}) })
.catch(err => { .catch(err => {
this.isBookmarksAvailable = false; this.isBookmarksAvailable = false;

View File

@ -64,6 +64,7 @@ $expand-color: $column-color;
& p { & p {
margin: 0px; margin: 0px;
white-space: pre-wrap;
//font-size: .9em; //font-size: .9em;
// font-size: 14px; // font-size: 14px;
} }

View File

@ -97,7 +97,7 @@ export class DatabindedTextComponent implements OnInit {
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0]; let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
let classname = this.getClassNameForHastag(extractedHashtag); let classname = this.getClassNameForHastag(extractedHashtag);
this.processedText += ` <a href="${extractedUrl}" class="${classname}" title="#${extractedHashtag}" target="_blank" rel="noopener noreferrer">#${extractedHashtag}</a>`; this.processedText += `<a href="${extractedUrl}" class="${classname}" title="#${extractedHashtag}" target="_blank" rel="noopener noreferrer">#${extractedHashtag}</a>`;
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1]; if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
this.hashtags.push(extractedHashtag); this.hashtags.push(extractedHashtag);
} }
@ -205,6 +205,10 @@ export class DatabindedTextComponent implements OnInit {
} }
ngAfterViewInit() { ngAfterViewInit() {
this.processEventBindings();
}
processEventBindings(){
for (const hashtag of this.hashtags) { for (const hashtag of this.hashtags) {
let classname = this.getClassNameForHastag(hashtag); let classname = this.getClassNameForHastag(hashtag);
let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`); let els = <Element[]>this.contentElement.nativeElement.querySelectorAll(`.${classname}`);

View File

@ -0,0 +1,6 @@
<div class="translation translation__button-display" *ngIf="isTranslationAvailable && showTranslationButton">
<a href class="translation__link translation__button-display__link" (click)="translate()">Translate</a>
</div>
<div class="translation translation__display" *ngIf="isTranslationAvailable && !showTranslationButton">
<span class="translation__by">Translated by {{translatedBy}}</span> <a href (click)="revertTranslation()" class="translation__link translation__display__link">revert</a>
</div>

View File

@ -0,0 +1,44 @@
@import "variables";
@import "commons";
$translation-color: #656b8f;
$translation-color-hover: #9fa5ca;
.translation {
margin: 0 10px 0 $avatar-column-space;
color: $translation-color;
font-size: 12px;
&__button-display {
text-align: center;
&__link {
display: block;
padding: 5px 5px 0 5px;
}
}
&__display {
display: flex;
justify-content: space-between;
&__link {
padding: 5px 0 0 0;
}
}
&__link {
color: $translation-color;
transition: all .2s;
&:hover {
text-decoration: none;
color: $translation-color-hover;
}
}
&__by {
display: block;
text-align: left;
padding: 5px 0 0 0;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StatusTranslateComponent } from './status-translate.component';
xdescribe('StatusTranslateComponent', () => {
let component: StatusTranslateComponent;
let fixture: ComponentFixture<StatusTranslateComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StatusTranslateComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StatusTranslateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,117 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs';
import { StatusWrapper } from '../../../../models/common.model';
import { ILanguage } from '../../../../states/settings.state';
import { LanguageService } from '../../../../services/language.service';
import { InstancesInfoService } from '../../../../services/instances-info.service';
import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service';
import { Translation } from '../../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../../services/notification.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-status-translate',
templateUrl: './status-translate.component.html',
styleUrls: ['./status-translate.component.scss']
})
export class StatusTranslateComponent implements OnInit, OnDestroy {
private languageSub: Subscription;
private languagesSub: Subscription;
private loadedTranslation: Translation;
selectedLanguage: ILanguage;
configuredLanguages: ILanguage[] = [];
isTranslationAvailable: boolean;
showTranslationButton: boolean = true;
translatedBy: string;
@Input() status: StatusWrapper;
@Output() translation = new EventEmitter<Translation>();
constructor(
private readonly mastodonWrapperService: MastodonWrapperService,
private readonly languageService: LanguageService,
private readonly instancesInfoService: InstancesInfoService,
private readonly notificationService: NotificationService
) { }
ngOnInit() {
this.languageSub = this.languageService.selectedLanguageChanged.subscribe(l => {
if (l) {
this.selectedLanguage = l;
this.analyseAvailability();
}
});
this.languagesSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
if (l) {
this.configuredLanguages = l;
this.analyseAvailability();
}
});
}
ngOnDestroy(): void {
if (this.languageSub) this.languageSub.unsubscribe();
if (this.languagesSub) this.languagesSub.unsubscribe();
}
private analyseAvailability() {
this.instancesInfoService.getTranslationAvailability(this.status.provider)
.then(canTranslate => {
if (canTranslate
&& !this.status.isRemote
&& this.configuredLanguages.length > 0
&& this.configuredLanguages.findIndex(x => x.iso639 === this.status.status.language) === -1) {
this.isTranslationAvailable = true;
}
else {
this.isTranslationAvailable = false;
}
})
.catch(err => {
console.error(err);
this.isTranslationAvailable = false;
});
}
translate(): boolean {
if(this.loadedTranslation){
this.translation.next(this.loadedTranslation);
this.showTranslationButton = false;
return false;
}
this.mastodonWrapperService.translate(this.status.provider, this.status.status.id, this.selectedLanguage.iso639)
.then(x => {
this.loadedTranslation = x;
this.translation.next(x);
this.translatedBy = x.provider;
this.showTranslationButton = false;
})
.catch((err: HttpErrorResponse) => {
console.error(err);
this.notificationService.notifyHttpError(err, this.status.provider);
});
return false;
}
revertTranslation(): boolean {
let revertTranslate: Translation;
revertTranslate = {
content: this.status.status.content,
language: this.loadedTranslation.detected_source_language,
detected_source_language: this.loadedTranslation.language,
provider: this.loadedTranslation.provider,
spoiler_text: this.status.status.spoiler_text
};
this.translation.next(revertTranslate);
this.showTranslationButton = true;
return false;
}
}

View File

@ -34,6 +34,17 @@
boosted your status boosted your status
</div> </div>
</div> </div>
<div *ngIf="notificationType === 'update'">
<div class="notification--icon">
<fa-icon class="update" [icon]="faEdit"></fa-icon>
</div>
<div class="notification--label">
<a href class="notification--link" title="{{ notificationAccount.acct }}"
(click)="openAccount(notificationAccount)"
(auxclick)="openUrl(notificationAccount.url)" innerHTML="{{ notificationAccount | accountEmoji }}"></a>
edited the status you boosted
</div>
</div>
<div *ngIf="notificationType === 'poll'"> <div *ngIf="notificationType === 'poll'">
<div class="notification--icon"> <div class="notification--icon">
<fa-icon class="boost" [icon]="faList"></fa-icon> <fa-icon class="boost" [icon]="faList"></fa-icon>
@ -98,10 +109,12 @@
<span class="status__content-warning--title">sensitive content</span> <span class="status__content-warning--title">sensitive content</span>
<span innerHTML="{{ contentWarningText }}"></span> <span innerHTML="{{ contentWarningText }}"></span>
</a> </a>
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected" <app-databinded-text #databindedtext class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)" (accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
(textSelected)="textSelected()"></app-databinded-text> (textSelected)="textSelected()"></app-databinded-text>
<app-status-translate [status]="displayedStatusWrapper" (translation)="onTranslation($event)"></app-status-translate>
<app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll" <app-poll class="status__poll" *ngIf="!isContentWarned && displayedStatus.poll"
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll> [poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>

View File

@ -258,6 +258,10 @@
color: $boost-color; color: $boost-color;
} }
.update {
color: $update-color;
}
.favorite { .favorite {
color: $favorite-color; color: $favorite-color;
} }
@ -272,4 +276,4 @@
&__label{ &__label{
color: $status-secondary-color; color: $status-secondary-color;
} }
} }

View File

@ -1,15 +1,15 @@
import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core"; import { Component, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from "@angular/core";
import { faStar, faRetweet, faList, faThumbtack } from "@fortawesome/free-solid-svg-icons"; import { faStar, faRetweet, faList, faThumbtack, faEdit } from "@fortawesome/free-solid-svg-icons";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { Status, Account } from "../../../services/models/mastodon.interfaces"; import { Status, Account, Translation } from "../../../services/models/mastodon.interfaces";
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service"; import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
import { ActionBarComponent } from "./action-bar/action-bar.component"; import { ActionBarComponent } from "./action-bar/action-bar.component";
import { StatusWrapper } from '../../../models/common.model'; import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools'; import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { ContentWarningPolicyEnum } from '../../../states/settings.state'; import { ContentWarningPolicyEnum } from '../../../states/settings.state';
import { StatusesStateService, StatusState } from "../../../services/statuses-state.service"; import { StatusesStateService, StatusState } from "../../../services/statuses-state.service";
import { DatabindedTextComponent } from "./databinded-text/databinded-text.component";
@Component({ @Component({
selector: "app-status", selector: "app-status",
@ -23,6 +23,7 @@ export class StatusComponent implements OnInit {
faRetweet = faRetweet; faRetweet = faRetweet;
faList = faList; faList = faList;
faThumbtack = faThumbtack; faThumbtack = faThumbtack;
faEdit = faEdit;
displayedStatus: Status; displayedStatus: Status;
displayedStatusWrapper: StatusWrapper; displayedStatusWrapper: StatusWrapper;
@ -52,7 +53,7 @@ export class StatusComponent implements OnInit {
@Input() isThreadDisplay: boolean; @Input() isThreadDisplay: boolean;
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll'; @Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
@Input() notificationAccount: Account; @Input() notificationAccount: Account;
private _statusWrapper: StatusWrapper; private _statusWrapper: StatusWrapper;
@ -106,27 +107,27 @@ export class StatusComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.statusesStateServiceSub = this.statusesStateService.stateNotification.subscribe(notification => { this.statusesStateServiceSub = this.statusesStateService.stateNotification.subscribe(notification => {
if(this._statusWrapper.status.url === notification.statusId && notification.isEdited) { if (this._statusWrapper.status.url === notification.statusId && notification.isEdited) {
this.statusWrapper = notification.editedStatus; this.statusWrapper = notification.editedStatus;
} }
}); });
} }
ngOnDestroy(){ ngOnDestroy() {
if(this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe(); if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
} }
private ensureMentionAreDisplayed(data: string): string { private ensureMentionAreDisplayed(data: string): string {
const mentions = this.displayedStatus.mentions; const mentions = this.displayedStatus.mentions;
if(!mentions || mentions.length === 0) return data; if (!mentions || mentions.length === 0) return data;
let textMentions = ''; let textMentions = '';
for (const m of mentions) { for (const m of mentions) {
if(!data.includes(m.url)){ if (!data.includes(m.url)) {
textMentions += `<span class="h-card"><a class="u-url mention" data-user="${m.id}" href="${m.url}" rel="ugc">@<span>${m.username}</span></a></span> ` textMentions += `<span class="h-card"><a class="u-url mention" data-user="${m.id}" href="${m.url}" rel="ugc">@<span>${m.username}</span></a></span> `
} }
} }
if(textMentions !== ''){ if (textMentions !== '') {
data = textMentions + data; data = textMentions + data;
} }
return data; return data;
@ -156,6 +157,31 @@ export class StatusComponent implements OnInit {
changeCw(cwIsActive: boolean) { changeCw(cwIsActive: boolean) {
this.isContentWarned = cwIsActive; this.isContentWarned = cwIsActive;
} }
@ViewChild('databindedtext') public databindedText: DatabindedTextComponent;
onTranslation(translation: Translation) {
let statusContent = translation.content;
// clean up a bit some issues (not reliable)
while (statusContent.includes('<span>@')) {
statusContent = statusContent.replace('<span>@', '@<span>');
}
while (statusContent.includes('h<span class="invisible">')){
statusContent = statusContent.replace('h<span class="invisible">', '<span class="invisible">h');
}
while (statusContent.includes('<span>#')){
statusContent = statusContent.replace('<span>#', '#<span>');
}
statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, statusContent, EmojiTypeEnum.medium);
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
setTimeout(x => {
this.databindedText.processEventBindings();
}, 500);
}
private checkLabels(status: Status) { private checkLabels(status: Status) {
//since API is limited with federated status... //since API is limited with federated status...

View File

@ -126,7 +126,7 @@ export class StreamNotificationsComponent extends BrowseBase {
this.loadMentions(userNotifications); this.loadMentions(userNotifications);
}); });
this.mastodonService.getNotifications(this.account, ['update'], null, null, 10) //FIXME: disable edition update until supported this.mastodonService.getNotifications(this.account, [], null, null, 10)
.then((notifications: Notification[]) => { .then((notifications: Notification[]) => {
this.isNotificationsLoading = false; this.isNotificationsLoading = false;

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ElectronService } from './electron.service';
describe('ElectronService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ElectronService = TestBed.get(ElectronService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MyElectronService {
constructor() {
}
setLang(lang: string) {
try {
if ((<any>window).api) {
(<any>window).api.send("changeSpellchecker", lang);
}
}
catch (err) {
console.error(err);
}
}
}

View File

@ -11,6 +11,7 @@ import { AccountInfo } from '../states/accounts.state';
export class InstancesInfoService { export class InstancesInfoService {
private defaultMaxChars = 500; private defaultMaxChars = 500;
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {}; private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
private cachedTranslationAvailability: { [id: string]: Promise<boolean>; } = {};
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {}; private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
constructor(private mastodonService: MastodonWrapperService) { } constructor(private mastodonService: MastodonWrapperService) { }
@ -65,4 +66,30 @@ export class InstancesInfoService {
} }
return this.cachedDefaultPrivacy[instance]; return this.cachedDefaultPrivacy[instance];
} }
getTranslationAvailability(account: AccountInfo): Promise<boolean> {
const instance = account.instance;
if (!this.cachedTranslationAvailability[instance]) {
this.cachedTranslationAvailability[instance] = this.mastodonService.getInstance(instance)
.then((instance: Instance) => {
if (+instance.version.split('.')[0] >= 4) {
const instanceV2 = <Instancev2>instance;
if (instanceV2
&& instanceV2.configuration
&& instanceV2.configuration.translation)
return instanceV2.configuration.translation.enabled;
} else {
const instanceV1 = <Instancev1>instance;
if (instanceV1 && instanceV1.max_toot_chars)
return false;
}
return false;
})
.catch(() => {
return false;
});
}
return this.cachedTranslationAvailability[instance];
}
} }

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { LanguageService } from './language.service';
xdescribe('LanguageService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: LanguageService = TestBed.get(LanguageService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,279 @@
import { T } from '@angular/cdk/keycodes';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ILanguage } from '../states/settings.state';
import { MyElectronService } from './electron.service';
import { SettingsService } from './settings.service';
@Injectable({
providedIn: 'root'
})
export class LanguageService {
configuredLanguagesChanged = new BehaviorSubject<ILanguage[]>([]);
selectedLanguageChanged = new BehaviorSubject<ILanguage>(null);
constructor(
private settingsService: SettingsService,
private electronService: MyElectronService
) {
this.configuredLanguagesChanged.next(this.getConfiguredLanguages());
this.selectedLanguageChanged.next(this.getSelectedLanguage());
}
getSelectedLanguage(): ILanguage {
const lang = this.settingsService.getSettings().selectedLanguage;
return lang;
}
setSelectedLanguage(lang: ILanguage): void {
var settings = this.settingsService.getSettings();
settings.selectedLanguage = lang;
this.settingsService.saveSettings(settings);
this.selectedLanguageChanged.next(lang);
if (lang) {
this.electronService.setLang(lang.iso639);
}
}
getConfiguredLanguages(): ILanguage[] {
const langs = this.settingsService.getSettings().configuredLanguages;
return langs;
}
addLanguage(lang: ILanguage) {
var settings = this.settingsService.getSettings();
settings.configuredLanguages.push(lang);
settings.configuredLanguages.sort((a, b) => a.name.localeCompare(b.name));
this.settingsService.saveSettings(settings);
this.configuredLanguagesChanged.next(settings.configuredLanguages);
if (settings.configuredLanguages.length === 1) {
this.setSelectedLanguage(lang);
}
}
removeLanguage(lang: ILanguage) {
var settings = this.settingsService.getSettings();
settings.configuredLanguages = settings.configuredLanguages.filter(x => x.iso639 !== lang.iso639);
this.settingsService.saveSettings(settings);
this.configuredLanguagesChanged.next(settings.configuredLanguages);
if (this.getSelectedLanguage().iso639 === lang.iso639) {
if (settings.configuredLanguages.length > 0) {
this.setSelectedLanguage(settings.configuredLanguages[0]);
} else {
this.setSelectedLanguage(null);
}
}
}
searchLanguage(input: string): ILanguage[] {
if (!input) return [];
const avLangs = this.getAllAvaialbleLaguages();
let found = avLangs.filter(x => x.name.toLowerCase().includes(input.toLowerCase()) || x.iso639.toLowerCase().includes(input.toLowerCase()));
found.sort((a, b) => a.name.localeCompare(b.name));
found = found.slice(0, 5);
return found;
}
private getAllAvaialbleLaguages(): Language[] {
return [
new Language("aa", "Afar"),
new Language("ab", "Abkhazian"),
new Language("af", "Afrikaans"),
new Language("ak", "Akan"),
new Language("am", "Amharic"),
new Language("an", "Aragonese"),
new Language("ar", "Arabic"),
new Language("as", "Assamese"),
new Language("av", "Avar"),
new Language("ay", "Aymara"),
new Language("az", "Azerbaijani"),
new Language("ba", "Bashkir"),
new Language("be", "Belarusian"),
new Language("bg", "Bulgarian"),
new Language("bh", "Bihari"),
new Language("bi", "Bislama"),
new Language("bm", "Bambara"),
new Language("bn", "Bengali"),
new Language("bo", "Tibetan"),
new Language("br", "Breton"),
new Language("bs", "Bosnian"),
new Language("ca", "Catalan"),
new Language("ce", "Chechen"),
new Language("ch", "Chamorro"),
new Language("co", "Corsican"),
new Language("cr", "Cree"),
new Language("cs", "Czech"),
new Language("cu", "Old Church Slavonic"),
new Language("cv", "Chuvash"),
new Language("cy", "Welsh"),
new Language("da", "Danish"),
new Language("de", "German"),
new Language("dv", "Divehi"),
new Language("dz", "Dzongkha"),
new Language("ee", "Ewe"),
new Language("el", "Greek"),
new Language("en", "English"),
new Language("eo", "Esperanto"),
new Language("es", "Spanish"),
new Language("et", "Estonian"),
new Language("eu", "Basque"),
new Language("fa", "Persian"),
new Language("ff", "Peul"),
new Language("fi", "Finnish"),
new Language("fj", "Fijian"),
new Language("fo", "Faroese"),
new Language("fr", "French"),
new Language("fy", "West Frisian"),
new Language("ga", "Irish"),
new Language("gd", "Scottish Gaelic"),
new Language("gl", "Galician"),
new Language("gn", "Guarani"),
new Language("gu", "Gujarati"),
new Language("gv", "Manx"),
new Language("ha", "Hausa"),
new Language("he", "Hebrew"),
new Language("hi", "Hindi"),
new Language("ho", "Hiri Motu"),
new Language("hr", "Croatian"),
new Language("ht", "Haitian"),
new Language("hu", "Hungarian"),
new Language("hy", "Armenian"),
new Language("hz", "Herero"),
new Language("ia", "Interlingua"),
new Language("id", "Indonesian"),
new Language("ie", "Interlingue"),
new Language("ig", "Igbo"),
new Language("ii", "Sichuan Yi"),
new Language("ik", "Inupiak"),
new Language("io", "Ido"),
new Language("is", "Icelandic"),
new Language("it", "Italian"),
new Language("iu", "Inuktitut"),
new Language("ja", "Japanese"),
new Language("jv", "Javanese"),
new Language("ka", "Georgian"),
new Language("kg", "Kongo"),
new Language("ki", "Kikuyu"),
new Language("kj", "Kuanyama"),
new Language("kk", "Kazakh"),
new Language("kl", "Greenlandic"),
new Language("km", "Cambodian"),
new Language("kn", "Kannada"),
new Language("ko", "Korean"),
new Language("kr", "Kanuri"),
new Language("ks", "Kashmiri"),
new Language("ku", "Kurdish"),
new Language("kv", "Komi"),
new Language("kw", "Cornish"),
new Language("ky", "Kirghiz"),
new Language("la", "Latin"),
new Language("lb", "Luxembourgish"),
new Language("lg", "Ganda"),
new Language("li", "Limburgian"),
new Language("ln", "Lingala"),
new Language("lo", "Laotian"),
new Language("lt", "Lithuanian"),
new Language("lu", "Luba-Katanga"),
new Language("lv", "Latvian"),
new Language("mg", "Malagasy"),
new Language("mh", "Marshallese"),
new Language("mi", "Maori"),
new Language("mk", "Macedonian"),
new Language("ml", "Malayalam"),
new Language("mn", "Mongolian"),
new Language("mo", "Moldovan"),
new Language("mr", "Marathi"),
new Language("ms", "Malay"),
new Language("mt", "Maltese"),
new Language("my", "Burmese"),
new Language("na", "Nauruan"),
new Language("nb", "Norwegian Bokmål"),
new Language("nd", "North Ndebele"),
new Language("ne", "Nepali"),
new Language("ng", "Ndonga"),
new Language("nl", "Dutch"),
new Language("nn", "Norwegian Nynorsk"),
new Language("no", "Norwegian"),
new Language("nr", "South Ndebele"),
new Language("nv", "Navajo"),
new Language("ny", "Chichewa"),
new Language("oc", "Occitan"),
new Language("oj", "Ojibwa"),
new Language("om", "Oromo"),
new Language("or", "Oriya"),
new Language("os", "Ossetian"),
new Language("pa", "Panjabi"),
new Language("pi", "Pali"),
new Language("pl", "Polish"),
new Language("ps", "Pashto"),
new Language("pt", "Portuguese"),
new Language("qu", "Quechua"),
new Language("rm", "Raeto Romance"),
new Language("rn", "Kirundi"),
new Language("ro", "Romanian"),
new Language("ru", "Russian"),
new Language("rw", "Rwandi"),
new Language("sa", "Sanskrit"),
new Language("sc", "Sardinian"),
new Language("sd", "Sindhi"),
new Language("se", "Northern Sami"),
new Language("sg", "Sango"),
new Language("sh", "Serbo-Croatian"),
new Language("si", "Sinhalese"),
new Language("sk", "Slovak"),
new Language("sl", "Slovenian"),
new Language("sm", "Samoan"),
new Language("sn", "Shona"),
new Language("so", "Somalia"),
new Language("sq", "Albanian"),
new Language("sr", "Serbian"),
new Language("ss", "Swati"),
new Language("st", "Southern Sotho"),
new Language("su", "Sundanese"),
new Language("sv", "Swedish"),
new Language("sw", "Swahili"),
new Language("ta", "Tamil"),
new Language("te", "Telugu"),
new Language("tg", "Tajik"),
new Language("th", "Thai"),
new Language("ti", "Tigrinya"),
new Language("tk", "Turkmen"),
new Language("tl", "Tagalog"),
new Language("tn", "Tswana"),
new Language("to", "Tonga"),
new Language("tr", "Turkish"),
new Language("ts", "Tsonga"),
new Language("tt", "Tatar"),
new Language("tw", "Twi"),
new Language("ty", "Tahitian"),
new Language("ug", "Uyghur"),
new Language("uk", "Ukrainian"),
new Language("ur", "Urdu"),
new Language("uz", "Uzbek"),
new Language("ve", "Venda"),
new Language("vi", "Vietnamese"),
new Language("vo", "Volapük"),
new Language("wa", "Walloon"),
new Language("wo", "Wolof"),
new Language("xh", "Xhosa"),
new Language("yi", "Yiddish"),
new Language("yo", "Yoruba"),
new Language("za", "Zhuang"),
new Language("zh", "Chinese"),
new Language("zu", "Zulu"),
];
}
}
export class Language {
constructor(public iso639: string, public name: string) {
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData, Tag } from "./models/mastodon.interfaces"; import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, TokenData, Tag, Translation } from "./models/mastodon.interfaces";
import { AccountInfo, UpdateAccount } from '../states/accounts.state'; import { AccountInfo, UpdateAccount } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state'; import { StreamTypeEnum, StreamElement } from '../states/streams.state';
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service'; import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
@ -96,6 +96,13 @@ export class MastodonWrapperService {
return this.mastodonService.getInstance(instance); return this.mastodonService.getInstance(instance);
} }
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation>{
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.translate(refreshedAccount, statusId, lang);
});
}
retrieveAccountDetails(account: AccountInfo): Promise<Account> { retrieveAccountDetails(account: AccountInfo): Promise<Account> {
return this.refreshAccountIfNeeded(account) return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => { .then((refreshedAccount: AccountInfo) => {
@ -117,17 +124,17 @@ export class MastodonWrapperService {
}); });
} }
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> { postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
return this.refreshAccountIfNeeded(account) return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => { .then((refreshedAccount: AccountInfo) => {
return this.mastodonService.postNewStatus(refreshedAccount, status, visibility, spoiler, in_reply_to_id, mediaIds, poll, scheduled_at); return this.mastodonService.postNewStatus(refreshedAccount, status, visibility, spoiler, in_reply_to_id, mediaIds, poll, scheduled_at, lang);
}); });
} }
editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> { editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
return this.refreshAccountIfNeeded(account) return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => { .then((refreshedAccount: AccountInfo) => {
return this.mastodonService.editStatus(refreshedAccount, statusId, status, visibility, spoiler, in_reply_to_id, attachements, poll, scheduled_at); return this.mastodonService.editStatus(refreshedAccount, statusId, status, visibility, spoiler, in_reply_to_id, attachements, poll, scheduled_at, lang);
}); });
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http'; import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings'; import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, Tag, Instancev2, Instancev1 } from "./models/mastodon.interfaces"; import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji, Conversation, ScheduledStatus, Tag, Instancev2, Instancev1, Translation } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state'; import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum, StreamElement } from '../states/streams.state'; import { StreamTypeEnum, StreamElement } from '../states/streams.state';
@ -21,6 +21,13 @@ export class MastodonService {
}); });
} }
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation>{
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
let route = `https://${account.instance}${this.apiRoutes.translate.replace('{0}', statusId)}`;
return this.httpClient.post<Translation>(route, { 'lang': lang }, { headers: headers }).toPromise();
}
retrieveAccountDetails(account: AccountInfo): Promise<Account> { retrieveAccountDetails(account: AccountInfo): Promise<Account> {
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise(); return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise();
@ -88,7 +95,7 @@ export class MastodonService {
return origString.replace(regEx, ""); return origString.replace(regEx, "");
}; };
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> { postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
const url = `https://${account.instance}${this.apiRoutes.postNewStatus}`; const url = `https://${account.instance}${this.apiRoutes.postNewStatus}`;
const statusData = new StatusData(); const statusData = new StatusData();
@ -106,10 +113,16 @@ export class MastodonService {
if (in_reply_to_id) { if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id; statusData.in_reply_to_id = in_reply_to_id;
} }
if (spoiler) { if (spoiler) {
statusData.sensitive = true; statusData.sensitive = true;
statusData.spoiler_text = spoiler; statusData.spoiler_text = spoiler;
} }
if(lang) {
statusData.language = lang;
}
switch (visibility) { switch (visibility) {
case VisibilityEnum.Public: case VisibilityEnum.Public:
statusData.visibility = 'public'; statusData.visibility = 'public';
@ -132,7 +145,7 @@ export class MastodonService {
return this.httpClient.post<Status>(url, statusData, { headers: headers }).toPromise(); 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, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null): Promise<Status> { editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, attachements: Attachment[], poll: PollParameters = null, scheduled_at: string = null, lang: string = null): Promise<Status> {
const url = `https://${account.instance}${this.apiRoutes.editStatus.replace('{0}', statusId)}`; const url = `https://${account.instance}${this.apiRoutes.editStatus.replace('{0}', statusId)}`;
const statusData = new StatusData(); const statusData = new StatusData();
@ -151,10 +164,16 @@ export class MastodonService {
if (in_reply_to_id) { if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id; statusData.in_reply_to_id = in_reply_to_id;
} }
if (spoiler) { if (spoiler) {
statusData.sensitive = true; statusData.sensitive = true;
statusData.spoiler_text = spoiler; statusData.spoiler_text = spoiler;
} }
if(lang) {
statusData.language = lang;
}
switch (visibility) { switch (visibility) {
case VisibilityEnum.Public: case VisibilityEnum.Public:
statusData.visibility = 'public'; statusData.visibility = 'public';
@ -651,6 +670,8 @@ class StatusData {
spoiler_text: string; spoiler_text: string;
visibility: string; visibility: string;
// scheduled_at: string; // scheduled_at: string;
language: string;
} }
class MediaAttributes { class MediaAttributes {

View File

@ -80,4 +80,5 @@ export class ApiRoutes {
followHashtag = '/api/v1/tags/{0}/follow'; followHashtag = '/api/v1/tags/{0}/follow';
unfollowHashtag = '/api/v1/tags/{0}/unfollow'; unfollowHashtag = '/api/v1/tags/{0}/unfollow';
getHashtag = '/api/v1/tags/{0}'; getHashtag = '/api/v1/tags/{0}';
translate = '/api/v1/statuses/{0}/translate';
} }

View File

@ -132,7 +132,8 @@ export interface Instancev2 extends Instance {
export interface Instancev2Configuration { export interface Instancev2Configuration {
urls: Instancev2Urls; urls: Instancev2Urls;
statuses: Instancev2Statuses statuses: Instancev2Statuses;
translation: Instancev2Translation;
} }
export interface InstanceUrls { export interface InstanceUrls {
@ -147,6 +148,10 @@ export interface Instancev2Statuses {
max_characters: number; max_characters: number;
} }
export interface Instancev2Translation {
enabled: boolean;
}
export interface Mention { export interface Mention {
url: string; url: string;
username: string; username: string;
@ -284,4 +289,12 @@ export interface Tag {
url: string; url: string;
history: TagHistory[]; history: TagHistory[];
following: boolean; following: boolean;
}
export interface Translation {
content: string;
language: string;
detected_source_language: string;
provider: string;
spoiler_text: string;
} }

View File

@ -33,6 +33,11 @@ export class SettingsService {
this.saveSettings(settings); this.saveSettings(settings);
} }
if(!settings.configuredLanguages){
settings.configuredLanguages = [];
this.saveSettings(settings);
}
return settings; return settings;
} }

View File

@ -96,7 +96,7 @@ export class StreamingWrapper {
} }
private pullNewNotifications() { private pullNewNotifications() {
this.mastodonService.getNotifications(this.account, ['update'], null, this.since_id_notifications, 10) this.mastodonService.getNotifications(this.account, [], null, this.since_id_notifications, 10)
.then((notifications: Notification[]) => { .then((notifications: Notification[]) => {
//notifications = notifications.sort((a, b) => a.id.localeCompare(b.id)); //notifications = notifications.sort((a, b) => a.id.localeCompare(b.id));
let soundMuted = !this.since_id_notifications; let soundMuted = !this.since_id_notifications;
@ -168,9 +168,6 @@ export class StreamingWrapper {
newUpdate.type = EventEnum.unknow; newUpdate.type = EventEnum.unknow;
} }
if(newUpdate.notification && newUpdate.notification.type === 'update') { //FIXME: disabling edition update until supported
return;
}
this.statusUpdateSubjet.next(newUpdate); this.statusUpdateSubjet.next(newUpdate);
} }

View File

@ -77,21 +77,47 @@ export class ToolsService {
return Promise.resolve(this.instanceInfos[acc.instance]); return Promise.resolve(this.instanceInfos[acc.instance]);
} else { } else {
return this.mastodonService.getInstance(acc.instance) return this.mastodonService.getInstance(acc.instance)
.then(instance => { .then(instance => {
let type = InstanceType.Mastodon;
if (instance.version.toLowerCase().includes('pleroma')) {
type = InstanceType.Pleroma;
} else if (instance.version.toLowerCase().includes('+glitch')) {
type = InstanceType.GlitchSoc;
} else if (instance.version.toLowerCase().includes('+florence')) {
type = InstanceType.Florence;
} else if (instance.version.toLowerCase().includes('pixelfed')) {
type = InstanceType.Pixelfed;
}
const splittedVersion = instance.version.split('.'); const splittedVersion = instance.version.split('.');
const major = +splittedVersion[0]; let major = +splittedVersion[0];
const minor = +splittedVersion[1]; let minor = +splittedVersion[1];
let altMajor = 0;
let altMinor = 0;
let type = InstanceType.Mastodon;
const version = instance.version.toLowerCase();
if (version.includes('pleroma')) {
type = InstanceType.Pleroma;
const pleromaVersion = version.split('pleroma ')[1].split('.');
altMajor = +pleromaVersion[0];
altMinor = +pleromaVersion[1];
} else if (version.includes('+glitch')) {
type = InstanceType.GlitchSoc;
} else if (version.includes('+florence')) {
type = InstanceType.Florence;
} else if (version.includes('pixelfed')) {
type = InstanceType.Pixelfed;
} else if (version.includes('takahe')) {
type = InstanceType.Takahe;
major = 1; //FIXME: when a clearer set of feature are available
minor = 0; //FIXME: when a clearer set of feature are available
const takaheVersion = version.split('takahe/')[1].split('.');
altMajor = +takaheVersion[0];
altMinor = +takaheVersion[1];
} else if (version.includes('akkoma')) {
type = InstanceType.Akkoma;
const akkomaVersion = version.split('akkoma ')[1].split('.');
altMajor = +akkomaVersion[0];
altMinor = +akkomaVersion[1];
}
let streamingApi = ""; let streamingApi = "";
@ -108,7 +134,7 @@ export class ToolsService {
streamingApi = instanceV1.urls.streaming_api; streamingApi = instanceV1.urls.streaming_api;
} }
let instanceInfo = new InstanceInfo(type, major, minor, streamingApi); let instanceInfo = new InstanceInfo(type, major, minor, streamingApi, altMajor, altMinor);
this.instanceInfos[acc.instance] = instanceInfo; this.instanceInfos[acc.instance] = instanceInfo;
return instanceInfo; return instanceInfo;
@ -116,6 +142,25 @@ export class ToolsService {
} }
} }
isBookmarksAreAvailable(account: AccountInfo): Promise<boolean> {
return this.getInstanceInfo(account)
.then((instance: InstanceInfo) => {
if (instance.major == 3 && instance.minor >= 1
|| instance.major > 3
|| instance.type === InstanceType.Pleroma && instance.altMajor >= 2 && instance.altMinor >= 5
|| instance.type === InstanceType.Akkoma && instance.altMajor >= 3 && instance.altMinor >= 9
|| instance.type === InstanceType.Takahe && instance.altMajor >= 0 && instance.altMinor >= 9) {
return true;
} else {
return false;
}
})
.catch(err => {
console.error(err);
return false;
});
}
getAvatar(acc: AccountInfo): Promise<string> { getAvatar(acc: AccountInfo): Promise<string> {
if (this.accountAvatar[acc.id]) { if (this.accountAvatar[acc.id]) {
return Promise.resolve(this.accountAvatar[acc.id]); return Promise.resolve(this.accountAvatar[acc.id]);
@ -247,16 +292,20 @@ export class InstanceInfo {
public readonly type: InstanceType, public readonly type: InstanceType,
public readonly major: number, public readonly major: number,
public readonly minor: number, public readonly minor: number,
public readonly streamingApi: string) { public readonly streamingApi: string,
public readonly altMajor: number,
public readonly altMinor: number) {
} }
} }
export enum InstanceType { export enum InstanceType {
Mastodon = 1, Mastodon = 1,
Pleroma = 2, Pleroma = 2, // "2.7.2 (compatible; Pleroma 2.5.1)"
GlitchSoc = 3, GlitchSoc = 3, // "4.1.5+glitch_0801_3b49b5a"
Florence = 4, Florence = 4,
Pixelfed = 5 Pixelfed = 5,
Takahe = 6, // "takahe/0.9.0"
Akkoma = 7, // "2.7.2 (compatible; Akkoma 3.9.2-develop)"
} }
export class StatusWithCwPolicyResult { export class StatusWithCwPolicyResult {

View File

@ -66,7 +66,7 @@ export class UserNotificationService {
this.notificationService.notifyHttpError(err, account); this.notificationService.notifyHttpError(err, account);
}); });
let getNotificationPromise = this.mastodonService.getNotifications(account, ['mention', 'update'], null, null, 10) let getNotificationPromise = this.mastodonService.getNotifications(account, ['mention'], null, null, 10)
.then((notifications: Notification[]) => { .then((notifications: Notification[]) => {
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserNotification); this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserNotification);
}) })

View File

@ -80,7 +80,15 @@ export class GlobalSettings {
columnSwitchingWinAlt = false; columnSwitchingWinAlt = false;
accountSettings: AccountSettings[] = []; accountSettings: AccountSettings[] = [];
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
}
export interface ILanguage {
iso639: string;
name: string;
} }
export interface SettingsStateModel { export interface SettingsStateModel {
@ -171,6 +179,8 @@ export class SettingsState {
newSettings.autoFollowOnListEnabled = oldSettings.autoFollowOnListEnabled; newSettings.autoFollowOnListEnabled = oldSettings.autoFollowOnListEnabled;
newSettings.twitterBridgeEnabled = oldSettings.twitterBridgeEnabled; newSettings.twitterBridgeEnabled = oldSettings.twitterBridgeEnabled;
newSettings.twitterBridgeInstance = oldSettings.twitterBridgeInstance; newSettings.twitterBridgeInstance = oldSettings.twitterBridgeInstance;
newSettings.configuredLanguages = oldSettings.configuredLanguages;
newSettings.selectedLanguage = oldSettings.selectedLanguage;
return newSettings; return newSettings;
} }

View File

@ -1,17 +1,27 @@
@import "variables"; @import "variables";
::ng-deep .ngx-contextmenu { ::ng-deep .ngx-contextmenu {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); $shadow: 0.4;
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
padding: 0;
border-radius: 7px;
overflow: hidden;
& .dropdown-menu { & .dropdown-menu {
//border: solid 1px $context-menu-border-color; //border: solid 1px $context-menu-border-color;
border: none; border: none;
background-color: $context-menu-background; background-color: $context-menu-background;
padding: 0; padding: 0;
margin: 0;
border-radius: 0px; border-radius: 0px;
border-radius: 7px;
overflow: hidden;
// padding: 2px 0; // padding: 2px 0;
// border-radius: 2px; // border-radius: 2px;
//border: solid 2px $context-menu-border-color; //border: solid 2px $context-menu-border-color;
@ -44,6 +54,6 @@
} }
& .divider { & .divider {
border-top: solid 2px $context-menu-border-color; border-top: solid 1px $context-menu-border-color;
} }
} }

View File

@ -21,6 +21,7 @@ $status-primary-color: #fff;
$status-secondary-color: #4e5572; $status-secondary-color: #4e5572;
$status-links-color: #d9e1e8; $status-links-color: #d9e1e8;
$boost-color : #5098eb; $boost-color : #5098eb;
$update-color : #95e470;
$favorite-color: #ffc16f; $favorite-color: #ffc16f;
$bookmarked-color: #ff5050; $bookmarked-color: #ff5050;
@ -52,9 +53,12 @@ $column-background: #0f111a;
$card-border-color: #2b344d; $card-border-color: #2b344d;
$context-menu-background: #d9e1e8; $context-menu-background: #d9e1e8;
$context-menu-background: #ffffff;
$context-menu-background-hover: #a9c9e6; $context-menu-background-hover: #a9c9e6;
$context-menu-background-hover: #d7dfeb;
$context-menu-font-color: #000000; $context-menu-font-color: #000000;
$context-menu-border-color: #c0cdd9; $context-menu-border-color: #c0cdd9;
$context-menu-border-color: #cbd3df;
$direct-message-background: #090a0f; $direct-message-background: #090a0f;