diff --git a/package-lock.json b/package-lock.json index 87ba2a57..d650fa7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sengi", - "version": "1.2.0", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 054950cf..8705cc8d 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 591f2991..64bfd416 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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([ diff --git a/src/app/components/create-status/create-status.component.html b/src/app/components/create-status/create-status.component.html index e6a5b1a2..caea73c2 100644 --- a/src/app/components/create-status/create-status.component.html +++ b/src/app/components/create-status/create-status.component.html @@ -1,16 +1,23 @@
- + - @@ -23,7 +30,7 @@ (suggestionSelectedEvent)="suggestionSelected($event)" (hasSuggestionsEvent)="suggestionsChanged($event)"> - + @@ -68,6 +75,10 @@ + +
+ You haven't set your language(s) yet, please go in the settings to provide it. +
@@ -83,5 +94,12 @@ Direct + + + + {{ l.name }} + + +
diff --git a/src/app/components/create-status/create-status.component.scss b/src/app/components/create-status/create-status.component.scss index a51d82e2..2a3ece36 100644 --- a/src/app/components/create-status/create-status.component.scss +++ b/src/app/components/create-status/create-status.component.scss @@ -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%; diff --git a/src/app/components/create-status/create-status.component.ts b/src/app/components/create-status/create-status.component.ts index 9ef54b66..240d878f 100644 --- a/src/app/components/create-status/create-status.component.ts +++ b/src/app/components/create-status/create-status.component.ts @@ -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; 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 { let parsedStatus = this.parseStatus(status); let resultPromise = Promise.resolve(previousStatus); @@ -630,9 +693,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy { let postPromise: Promise; 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; diff --git a/src/app/components/create-status/poll-editor/poll-editor.component.ts b/src/app/components/create-status/poll-editor/poll-editor.component.ts index 36f230bf..7b419fac 100644 --- a/src/app/components/create-status/poll-editor/poll-editor.component.ts +++ b/src/app/components/create-status/poll-editor/poll-editor.component.ts @@ -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 { diff --git a/src/app/components/floating-column/manage-account/manage-account.component.ts b/src/app/components/floating-column/manage-account/manage-account.component.ts index 1b6cb365..a4b8f27d 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.ts +++ b/src/app/components/floating-column/manage-account/manage-account.component.ts @@ -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; diff --git a/src/app/components/floating-column/manage-account/notifications/notification/notification.component.html b/src/app/components/floating-column/manage-account/notifications/notification/notification.component.html index 93d2f28d..59bc569c 100644 --- a/src/app/components/floating-column/manage-account/notifications/notification/notification.component.html +++ b/src/app/components/floating-column/manage-account/notifications/notification/notification.component.html @@ -22,7 +22,7 @@ (click)="acceptFollowRequest()"> - @@ -69,12 +69,18 @@ - + + + + + - \ No newline at end of file diff --git a/src/app/components/floating-column/manage-account/notifications/notifications.component.ts b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts index f766ca70..c1e8bf60 100644 --- a/src/app/components/floating-column/manage-account/notifications/notifications.component.ts +++ b/src/app/components/floating-column/manage-account/notifications/notifications.component.ts @@ -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; } diff --git a/src/app/components/floating-column/search/search.component.html b/src/app/components/floating-column/search/search.component.html index c085df15..7b884115 100644 --- a/src/app/components/floating-column/search/search.component.html +++ b/src/app/components/floating-column/search/search.component.html @@ -4,8 +4,8 @@

search

- +
diff --git a/src/app/components/floating-column/search/search.component.ts b/src/app/components/floating-column/search/search.component.ts index 4f0fc492..52b97578 100644 --- a/src/app/components/floating-column/search/search.component.ts +++ b/src/app/components/floating-column/search/search.component.ts @@ -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(); @Output() browseThreadEvent = new EventEmitter(); + @ViewChild('search') searchElement: ElementRef; + constructor( private readonly notificationService: NotificationService, private readonly toolsService: ToolsService, private readonly mastodonService: MastodonWrapperService) { } ngOnInit() { + this.searchElement.nativeElement.focus(); } onSubmit(): boolean { diff --git a/src/app/components/floating-column/settings/settings.component.html b/src/app/components/floating-column/settings/settings.component.html index 45f8ecf7..b6ffe302 100644 --- a/src/app/components/floating-column/settings/settings.component.html +++ b/src/app/components/floating-column/settings/settings.component.html @@ -35,7 +35,7 @@ play - +

Shortcuts

switch column:
@@ -51,21 +51,42 @@
+

Languages

+
+
+
+ No language set. +
+
+ {{ l.name }} ({{l.iso639}}) + remove +
+ +
+ {{ l.name }} ({{l.iso639}}) + add +
+
+

Twitter Bridge

+ (change)="onTwitterBridgeEnabledChanged()" type="checkbox" name="onTwitterBridgeEnabled" + value="onTwitterBridgeEnabled" id="onTwitterBridgeEnabled">

Please provide your bridge instance: - - If you don't know any, consider using BirdsiteLIVE

+ + If you don't know any, consider using BirdsiteLIVE +

@@ -79,7 +100,7 @@ - +
but add CW on content containing:
diff --git a/src/app/components/floating-column/settings/settings.component.scss b/src/app/components/floating-column/settings/settings.component.scss index f2fbd754..cd272593 100644 --- a/src/app/components/floating-column/settings/settings.component.scss +++ b/src/app/components/floating-column/settings/settings.component.scss @@ -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; diff --git a/src/app/components/floating-column/settings/settings.component.ts b/src/app/components/floating-column/settings/settings.component.ts index 7c364388..051b23f5 100644 --- a/src/app/components/floating-column/settings/settings.component.ts +++ b/src/app/components/floating-column/settings/settings.component.ts @@ -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) { diff --git a/src/app/components/stream/hashtag/hashtag.component.scss b/src/app/components/stream/hashtag/hashtag.component.scss index cdf3620f..07521008 100644 --- a/src/app/components/stream/hashtag/hashtag.component.scss +++ b/src/app/components/stream/hashtag/hashtag.component.scss @@ -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; diff --git a/src/app/components/stream/status/action-bar/action-bar.component.ts b/src/app/components/stream/status/action-bar/action-bar.component.ts index 6d279743..b067fbf5 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.ts +++ b/src/app/components/stream/status/action-bar/action-bar.component.ts @@ -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; diff --git a/src/app/components/stream/status/databinded-text/databinded-text.component.scss b/src/app/components/stream/status/databinded-text/databinded-text.component.scss index 32946ee5..cdbfa121 100644 --- a/src/app/components/stream/status/databinded-text/databinded-text.component.scss +++ b/src/app/components/stream/status/databinded-text/databinded-text.component.scss @@ -64,6 +64,7 @@ $expand-color: $column-color; & p { margin: 0px; + white-space: pre-wrap; //font-size: .9em; // font-size: 14px; } diff --git a/src/app/components/stream/status/databinded-text/databinded-text.component.ts b/src/app/components/stream/status/databinded-text/databinded-text.component.ts index f9fe2ec0..f5de70de 100644 --- a/src/app/components/stream/status/databinded-text/databinded-text.component.ts +++ b/src/app/components/stream/status/databinded-text/databinded-text.component.ts @@ -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 += ` #${extractedHashtag}`; + this.processedText += `#${extractedHashtag}`; 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 = this.contentElement.nativeElement.querySelectorAll(`.${classname}`); diff --git a/src/app/components/stream/status/status-translate/status-translate.component.html b/src/app/components/stream/status/status-translate/status-translate.component.html new file mode 100644 index 00000000..9314ba13 --- /dev/null +++ b/src/app/components/stream/status/status-translate/status-translate.component.html @@ -0,0 +1,6 @@ + +
+ Translated by {{translatedBy}} revert +
diff --git a/src/app/components/stream/status/status-translate/status-translate.component.scss b/src/app/components/stream/status/status-translate/status-translate.component.scss new file mode 100644 index 00000000..59d1765c --- /dev/null +++ b/src/app/components/stream/status/status-translate/status-translate.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/app/components/stream/status/status-translate/status-translate.component.spec.ts b/src/app/components/stream/status/status-translate/status-translate.component.spec.ts new file mode 100644 index 00000000..af6146f7 --- /dev/null +++ b/src/app/components/stream/status/status-translate/status-translate.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ StatusTranslateComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(StatusTranslateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/stream/status/status-translate/status-translate.component.ts b/src/app/components/stream/status/status-translate/status-translate.component.ts new file mode 100644 index 00000000..87d55804 --- /dev/null +++ b/src/app/components/stream/status/status-translate/status-translate.component.ts @@ -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(); + + 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; + } +} diff --git a/src/app/components/stream/status/status.component.html b/src/app/components/stream/status/status.component.html index be44235a..7257da73 100644 --- a/src/app/components/stream/status/status.component.html +++ b/src/app/components/stream/status/status.component.html @@ -34,6 +34,17 @@ boosted your status
+
+
+ +
+
+ + edited the status you boosted +
+
@@ -98,10 +109,12 @@ sensitive content - + + diff --git a/src/app/components/stream/status/status.component.scss b/src/app/components/stream/status/status.component.scss index aa1796c5..da273f0e 100644 --- a/src/app/components/stream/status/status.component.scss +++ b/src/app/components/stream/status/status.component.scss @@ -258,6 +258,10 @@ color: $boost-color; } +.update { + color: $update-color; +} + .favorite { color: $favorite-color; } @@ -272,4 +276,4 @@ &__label{ color: $status-secondary-color; } -} +} \ No newline at end of file diff --git a/src/app/components/stream/status/status.component.ts b/src/app/components/stream/status/status.component.ts index 9cea97a7..6782037f 100644 --- a/src/app/components/stream/status/status.component.ts +++ b/src/app/components/stream/status/status.component.ts @@ -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 += `@${m.username} ` } } - 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('@')) { + statusContent = statusContent.replace('@', '@'); + } + while (statusContent.includes('h