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",
"version": "1.2.0",
"version": "1.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@ -5,8 +5,7 @@ import { HttpModule } from "@angular/http";
import { HttpClientModule } from '@angular/common/http';
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
// import { NgxElectronModule } from "ngx-electron";
// import { NgxElectronModule } from 'ngx-electron';
import { NgxsModule } from '@ngxs/store';
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 { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-tutorial/labels-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 = [
{ path: "", component: StreamsMainDisplayComponent },
@ -159,7 +159,8 @@ const routes: Routes = [
TutorialEnhancedComponent,
NotificationsTutorialComponent,
LabelsTutorialComponent,
ThankyouTutorialComponent
ThankyouTutorialComponent,
StatusTranslateComponent
],
entryComponents: [
EmojiPickerComponent
@ -176,6 +177,7 @@ const routes: Routes = [
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
// NgxElectronModule,
RouterModule.forRoot(routes),
NgxsModule.forRoot([

View File

@ -1,16 +1,23 @@
<form class="status-editor" (ngSubmit)="onSubmit()">
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
<input #mytitle [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title"
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"
#emojiButton href (click)="openEmojiPicker($event)">
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
</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?"
(keydown.control.enter)="onCtrlEnter()"
(keydown.meta.enter)="onCtrlEnter()"
(keydown.escape)="reply.blur()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
</textarea>
@ -23,7 +30,7 @@
(suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)">
</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>
@ -68,6 +75,10 @@
<fa-icon [icon]="faClock"></fa-icon>
</a>
</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>
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
@ -83,5 +94,12 @@
<fa-icon [icon]="faEnvelope" class="context-menu-icon"></fa-icon> Direct
</ng-template>
</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>
</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 {
border-width: 0;
background-color: $status-editor-background;
@ -207,6 +233,20 @@ $counter-width: 90px;
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';
// ::ng-deep .cdk-overlay-backdrop {
// // width: 100%;

View File

@ -11,7 +11,7 @@ import { ContextMenuService, ContextMenuComponent } from 'ngx-contextmenu';
import { VisibilityEnum, PollParameters } from '../../services/mastodon.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 { NotificationService } from '../../services/notification.service';
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 { StatusesStateService } from '../../services/statuses-state.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({
selector: 'app-create-status',
@ -140,9 +143,19 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
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;
charCountLeft: number;
postCounts: number = 1;
@ -153,6 +166,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
instanceSupportsScheduling = true;
isEditing: boolean;
editingStatusId: string;
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
private statusLoaded: boolean;
private hasSuggestions: boolean;
@ -162,6 +177,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
@ViewChild('fileInput') fileInputElement: ElementRef;
@ViewChild('footer') footerElement: ElementRef;
@ViewChild(ContextMenuComponent) public contextMenu: ContextMenuComponent;
@ViewChild('langContextMenu') public langContextMenu: ContextMenuComponent;
@ViewChild(PollEditorComponent) pollEditor: PollEditorComponent;
@ViewChild(StatusSchedulerComponent) statusScheduler: StatusSchedulerComponent;
@ -196,11 +212,15 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
private langSub: Subscription;
private selectLangSub: Subscription;
private selectedAccount: AccountInfo;
constructor(
private readonly navigationService: NavigationService,
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService,
private statusStateService: StatusesStateService,
private readonly statusStateService: StatusesStateService,
private readonly scheduledStatusService: ScheduledStatusService,
private readonly contextMenuService: ContextMenuService,
private readonly store: Store,
@ -216,7 +236,35 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
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() {
this.initLanguages();
if (!this.isRedrafting) {
this.status = this.statusStateService.getStatusContent(this.statusReplyingToWrapper);
}
@ -263,6 +311,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
this.accountSub.unsubscribe();
this.langSub.unsubscribe();
this.selectLangSub.unsubscribe();
}
onNavigateToSettings(): boolean {
this.navigationService.openPanel(LeftPanelType.Settings);
return false;
}
onPaste(e: any) {
@ -613,6 +668,14 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
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> {
let parsedStatus = this.parseStatus(status);
let resultPromise = Promise.resolve(previousStatus);
@ -630,9 +693,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
let postPromise: Promise<Status>;
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 {
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
@ -642,9 +705,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
});
} else {
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 {
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();
}
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
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
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 { PollEntry } from './poll-entry/poll-entry.component';
import { PollParameters } from '../../../services/mastodon.service';
import { retry } from 'rxjs/operators';
import { Poll } from '../../../services/models/mastodon.interfaces';
@Component({
selector: 'app-poll-editor',
@ -19,6 +19,8 @@ export class PollEditorComponent implements OnInit {
selectedId: string;
private multiSelected: boolean;
@Input() oldPoll: Poll;
constructor() {
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 {
this.entryUuid++;
return this.entryUuid;
@ -50,7 +58,7 @@ export class PollEditorComponent implements OnInit {
return false;
}
removeElement(entry: PollEntry){
removeElement(entry: PollEntry) {
this.entries = this.entries.filter(x => x.id != entry.id);
}
@ -69,6 +77,17 @@ export class PollEditorComponent implements OnInit {
params.hide_totals = false;
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 {

View File

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

View File

@ -22,7 +22,7 @@
(click)="acceptFollowRequest()">
<fa-icon class="follow_request__icon" [icon]="faCheck"></fa-icon>
</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()">
<fa-icon class="follow_request__icon" [icon]="faTimes"></fa-icon>
</a>
@ -69,12 +69,18 @@
</a>
</div>
<app-status *ngIf="notification.status && notification.type !== 'mention'" 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 === '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>
<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>

View File

@ -101,7 +101,7 @@ export class NotificationsComponent extends BrowseBase {
this.isLoading = 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[]) => {
if (notifications.length === 0) {
this.maxReached = true;
@ -152,6 +152,7 @@ export class NotificationWrapper {
case 'reblog':
case 'favourite':
case 'poll':
case 'update':
this.status = new StatusWrapper(notification.status, provider, applyCw, hideStatus);
break;
}

View File

@ -4,8 +4,8 @@
<h3 class="panel__title">search</h3>
<form class="form-section" (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" />
<input #search type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" (keydown.escape)="search.blur()"/>
<button class="form-button" type="submit" title="search">GO</button>
</form>
</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 { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
@ -26,12 +26,15 @@ export class SearchComponent implements OnInit {
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@ViewChild('search') searchElement: ElementRef;
constructor(
private readonly notificationService: NotificationService,
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonWrapperService) { }
ngOnInit() {
this.searchElement.nativeElement.focus();
}
onSubmit(): boolean {

View File

@ -35,7 +35,7 @@
</form>
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
</div>
<h4 class="panel__subtitle">Shortcuts</h4>
<div class="sub-section">
<span class="sub-section__title">switch column:</span><br />
@ -51,21 +51,42 @@
<br>
</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>
<div class="sub-section">
<input class="sub-section__checkbox" [(ngModel)]="twitterBridgeEnabled"
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
(change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled"
value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">
<label class="noselect sub-section__label" for="onTwitterBridgeEnabled">enable bridge</label>
<br>
<div *ngIf="twitterBridgeEnabled">
<p>Please provide your bridge instance:
<input type="text" class="form-control form-control-sm sub_section__text-input"
[(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>
<input type="text" class="form-control form-control-sm sub_section__text-input"
[(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>
</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>
@ -79,7 +100,7 @@
<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">
<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>
<div class="sub-section__cw-settings" *ngIf="contentWarningPolicy === 2">
<span class="sub-section__title">but add CW on content containing:</span><br />

View File

@ -31,6 +31,13 @@
padding: 0 5px 15px 5px;
position: relative;
&__content {
display: block;
padding: 0 0 0 5px;
// outline: 1px dotted greenyellow;
}
&__checkbox {
position: relative;
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 {
border: 1px solid $settings-text-input-border;
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 { Howl } from 'howler';
import { Subscription } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { ToolsService, InstanceType } from '../../../services/tools.service';
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.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 { NavigationService } from '../../../services/navigation.service';
import { SettingsService } from '../../../services/settings.service';
import { LanguageService } from '../../../services/language.service';
@Component({
selector: 'app-settings',
@ -17,7 +19,7 @@ import { SettingsService } from '../../../services/settings.service';
styleUrls: ['./settings.component.scss']
})
export class SettingsComponent implements OnInit {
export class SettingsComponent implements OnInit, OnDestroy {
notificationSounds: NotificationSoundDefinition[];
notificationSoundId: string;
@ -39,6 +41,10 @@ export class SettingsComponent implements OnInit {
timeLineMode: TimeLineModeEnum = TimeLineModeEnum.OnTop;
contentWarningPolicy: ContentWarningPolicyEnum = ContentWarningPolicyEnum.None;
configuredLangs: ILanguage[] = [];
searchedLangs: ILanguage[] = [];
searchLang: string;
private addCwOnContent: string;
set setAddCwOnContent(value: string) {
this.setCwPolicy(null, value, null, null);
@ -76,16 +82,25 @@ export class SettingsComponent implements OnInit {
return this.twitterBridgeInstance;
}
private languageSub: Subscription;
constructor(
private readonly languageService: LanguageService,
private readonly settingsService: SettingsService,
private readonly navigationService: NavigationService,
private formBuilder: FormBuilder,
private serviceWorkersService: ServiceWorkerService,
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly userNotificationsService: UserNotificationService) { }
private readonly userNotificationsService: UserNotificationService) { }
ngOnInit() {
this.languageSub = this.languageService.configuredLanguagesChanged.subscribe(l => {
if(l){
this.configuredLangs = l;
}
});
this.version = environment.VERSION;
const settings = this.settingsService.getSettings();
@ -129,6 +144,34 @@ export class SettingsComponent implements OnInit {
this.twitterBridgeEnabled = settings.twitterBridgeEnabled;
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) {

View File

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

View File

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

View File

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

View File

@ -97,7 +97,7 @@ export class DatabindedTextComponent implements OnInit {
let extractedUrl = extractedLinkAndNext[0].split('href="')[1].split('"')[0];
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];
this.hashtags.push(extractedHashtag);
}
@ -205,6 +205,10 @@ export class DatabindedTextComponent implements OnInit {
}
ngAfterViewInit() {
this.processEventBindings();
}
processEventBindings(){
for (const hashtag of this.hashtags) {
let classname = this.getClassNameForHastag(hashtag);
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
</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 class="notification--icon">
<fa-icon class="boost" [icon]="faList"></fa-icon>
@ -98,10 +109,12 @@
<span class="status__content-warning--title">sensitive content</span>
<span innerHTML="{{ contentWarningText }}"></span>
</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)"
(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"
[poll]="displayedStatus.poll" [statusWrapper]="displayedStatusWrapper"></app-poll>

View File

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

View File

@ -1,15 +1,15 @@
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 { Status, Account } from "../../../services/models/mastodon.interfaces";
import { Status, Account, Translation } from "../../../services/models/mastodon.interfaces";
import { OpenThreadEvent, ToolsService } from "../../../services/tools.service";
import { ActionBarComponent } from "./action-bar/action-bar.component";
import { StatusWrapper } from '../../../models/common.model';
import { EmojiConverter, EmojiTypeEnum } from '../../../tools/emoji.tools';
import { ContentWarningPolicyEnum } from '../../../states/settings.state';
import { StatusesStateService, StatusState } from "../../../services/statuses-state.service";
import { DatabindedTextComponent } from "./databinded-text/databinded-text.component";
@Component({
selector: "app-status",
@ -23,6 +23,7 @@ export class StatusComponent implements OnInit {
faRetweet = faRetweet;
faList = faList;
faThumbtack = faThumbtack;
faEdit = faEdit;
displayedStatus: Status;
displayedStatusWrapper: StatusWrapper;
@ -52,7 +53,7 @@ export class StatusComponent implements OnInit {
@Input() isThreadDisplay: boolean;
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll';
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
@Input() notificationAccount: Account;
private _statusWrapper: StatusWrapper;
@ -106,27 +107,27 @@ export class StatusComponent implements OnInit {
ngOnInit() {
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;
}
});
}
ngOnDestroy(){
if(this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
ngOnDestroy() {
if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
}
private ensureMentionAreDisplayed(data: string): string {
const mentions = this.displayedStatus.mentions;
if(!mentions || mentions.length === 0) return data;
if (!mentions || mentions.length === 0) return data;
let textMentions = '';
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> `
}
}
if(textMentions !== ''){
if (textMentions !== '') {
data = textMentions + data;
}
return data;
@ -156,6 +157,31 @@ export class StatusComponent implements OnInit {
changeCw(cwIsActive: boolean) {
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) {
//since API is limited with federated status...

View File

@ -126,7 +126,7 @@ export class StreamNotificationsComponent extends BrowseBase {
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[]) => {
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 {
private defaultMaxChars = 500;
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
private cachedTranslationAvailability: { [id: string]: Promise<boolean>; } = {};
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
constructor(private mastodonService: MastodonWrapperService) { }
@ -65,4 +66,30 @@ export class InstancesInfoService {
}
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 { 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 { StreamTypeEnum, StreamElement } from '../states/streams.state';
import { FavoriteResult, VisibilityEnum, PollParameters, MastodonService, BookmarkResult, FollowingResult } from './mastodon.service';
@ -96,6 +96,13 @@ export class MastodonWrapperService {
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> {
return this.refreshAccountIfNeeded(account)
.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)
.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)
.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 { 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 { 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> {
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
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, "");
};
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 statusData = new StatusData();
@ -106,10 +113,16 @@ export class MastodonService {
if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id;
}
if (spoiler) {
statusData.sensitive = true;
statusData.spoiler_text = spoiler;
}
if(lang) {
statusData.language = lang;
}
switch (visibility) {
case VisibilityEnum.Public:
statusData.visibility = 'public';
@ -132,7 +145,7 @@ export class MastodonService {
return this.httpClient.post<Status>(url, statusData, { headers: headers }).toPromise();
}
editStatus(account: AccountInfo, statusId: string, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, 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 statusData = new StatusData();
@ -151,10 +164,16 @@ export class MastodonService {
if (in_reply_to_id) {
statusData.in_reply_to_id = in_reply_to_id;
}
if (spoiler) {
statusData.sensitive = true;
statusData.spoiler_text = spoiler;
}
if(lang) {
statusData.language = lang;
}
switch (visibility) {
case VisibilityEnum.Public:
statusData.visibility = 'public';
@ -651,6 +670,8 @@ class StatusData {
spoiler_text: string;
visibility: string;
// scheduled_at: string;
language: string;
}
class MediaAttributes {

View File

@ -80,4 +80,5 @@ export class ApiRoutes {
followHashtag = '/api/v1/tags/{0}/follow';
unfollowHashtag = '/api/v1/tags/{0}/unfollow';
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 {
urls: Instancev2Urls;
statuses: Instancev2Statuses
statuses: Instancev2Statuses;
translation: Instancev2Translation;
}
export interface InstanceUrls {
@ -147,6 +148,10 @@ export interface Instancev2Statuses {
max_characters: number;
}
export interface Instancev2Translation {
enabled: boolean;
}
export interface Mention {
url: string;
username: string;
@ -284,4 +289,12 @@ export interface Tag {
url: string;
history: TagHistory[];
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);
}
if(!settings.configuredLanguages){
settings.configuredLanguages = [];
this.saveSettings(settings);
}
return settings;
}

View File

@ -96,7 +96,7 @@ export class StreamingWrapper {
}
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[]) => {
//notifications = notifications.sort((a, b) => a.id.localeCompare(b.id));
let soundMuted = !this.since_id_notifications;
@ -168,9 +168,6 @@ export class StreamingWrapper {
newUpdate.type = EventEnum.unknow;
}
if(newUpdate.notification && newUpdate.notification.type === 'update') { //FIXME: disabling edition update until supported
return;
}
this.statusUpdateSubjet.next(newUpdate);
}

View File

@ -77,21 +77,47 @@ export class ToolsService {
return Promise.resolve(this.instanceInfos[acc.instance]);
} else {
return this.mastodonService.getInstance(acc.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;
}
.then(instance => {
const splittedVersion = instance.version.split('.');
const major = +splittedVersion[0];
const minor = +splittedVersion[1];
let major = +splittedVersion[0];
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 = "";
@ -108,7 +134,7 @@ export class ToolsService {
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;
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> {
if (this.accountAvatar[acc.id]) {
return Promise.resolve(this.accountAvatar[acc.id]);
@ -247,16 +292,20 @@ export class InstanceInfo {
public readonly type: InstanceType,
public readonly major: number,
public readonly minor: number,
public readonly streamingApi: string) {
public readonly streamingApi: string,
public readonly altMajor: number,
public readonly altMinor: number) {
}
}
export enum InstanceType {
Mastodon = 1,
Pleroma = 2,
GlitchSoc = 3,
Pleroma = 2, // "2.7.2 (compatible; Pleroma 2.5.1)"
GlitchSoc = 3, // "4.1.5+glitch_0801_3b49b5a"
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 {

View File

@ -66,7 +66,7 @@ export class UserNotificationService {
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[]) => {
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserNotification);
})

View File

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

View File

@ -1,17 +1,27 @@
@import "variables";
::ng-deep .ngx-contextmenu {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
-o-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
$shadow: 0.4;
box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-moz-box-shadow: 0 0 10px rgba(0, 0, 0, $shadow);
-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 {
//border: solid 1px $context-menu-border-color;
border: none;
background-color: $context-menu-background;
padding: 0;
margin: 0;
border-radius: 0px;
border-radius: 7px;
overflow: hidden;
// padding: 2px 0;
// border-radius: 2px;
//border: solid 2px $context-menu-border-color;
@ -44,6 +54,6 @@
}
& .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-links-color: #d9e1e8;
$boost-color : #5098eb;
$update-color : #95e470;
$favorite-color: #ffc16f;
$bookmarked-color: #ff5050;
@ -52,9 +53,12 @@ $column-background: #0f111a;
$card-border-color: #2b344d;
$context-menu-background: #d9e1e8;
$context-menu-background: #ffffff;
$context-menu-background-hover: #a9c9e6;
$context-menu-background-hover: #d7dfeb;
$context-menu-font-color: #000000;
$context-menu-border-color: #c0cdd9;
$context-menu-border-color: #cbd3df;
$direct-message-background: #090a0f;