Compare commits

...

39 Commits

Author SHA1 Message Date
Nicolas Constant 822ef21985
Merge pull request #639 from NicolasConstant/develop
1.7.0 PR
2024-03-09 15:32:25 -05:00
Nicolas Constant a154028a53 road to 1.7.0 2024-03-09 15:10:04 -05:00
Nicolas Constant 6a8d85f40c
Merge pull request #638 from NicolasConstant/topic_enhance-filters
Topic enhance filters
2024-03-09 02:36:17 -05:00
Nicolas Constant 04153543a9 fix filtering message 2024-03-09 02:00:05 -05:00
Nicolas Constant 92ec089eab fix error spamming, fix #605 2024-03-09 01:46:26 -05:00
Nicolas Constant 12ce0a3a4a added context filtering 2024-03-09 00:40:37 -05:00
Nicolas Constant 7a6eb9c3d2 starting handling server filters 2024-03-08 03:13:58 -05:00
Nicolas Constant 63b7c6fdf1 added filter params in status interface 2024-03-08 02:12:00 -05:00
Nicolas Constant bd75317417 retrieve full handle on post edition, fix #630 2024-03-08 01:53:28 -05:00
Nicolas Constant 74eed7e8ba fix pleroma vote count, fix #398 2024-03-08 00:51:27 -05:00
Nicolas Constant ebce6282c5 better follow workflow, fix #629 2024-03-08 00:19:39 -05:00
Nicolas Constant 702e4daa44
Merge pull request #637 from NicolasConstant/topic_refine-css-zones
Topic refine css zones
2024-03-07 18:44:40 -05:00
Nicolas Constant d2221d539c fix undefined image description, fix #632 2024-03-07 18:23:26 -05:00
Nicolas Constant c4de387f86 sort list in alphabetical order, fix #633 2024-03-07 01:17:04 -05:00
Nicolas Constant c0f84ddc11 open status from image galery, fix #627 2024-03-07 00:59:00 -05:00
Nicolas Constant 1830212a91 flex migration of account navigation 2024-03-07 00:24:27 -05:00
Nicolas Constant 46adf207bb flex migration floating column + hover timeline 2024-03-06 23:13:46 -05:00
Nicolas Constant 909b190b33 fix tests 2024-03-06 19:14:30 -05:00
Nicolas Constant cfc4d5f915 GoToSocial max char handling, fix #576 2024-03-05 18:15:43 -05:00
Nicolas Constant 0f58252c61
Merge pull request #614 from NicolasConstant/develop
1.6.0 PR
2023-08-24 00:30:44 -04:00
Nicolas Constant 0d2ac6b569
road to 1.6.0 2023-08-23 23:43:50 -04:00
Nicolas Constant e62987b11a
Merge pull request #611 from NicolasConstant/topic_reorganize-accounts
Topic reorganize accounts
2023-08-20 03:35:34 -04:00
Nicolas Constant 8cee7289eb
Merge pull request #612 from NicolasConstant/topic_add-more-user-actions
Topic add more user actions
2023-08-20 03:33:32 -04:00
Nicolas Constant 0305cc6ac7
added button to unlock icons, fix #489 2023-08-20 02:33:03 -04:00
Nicolas Constant f215d027f9
added draggable aspect in service 2023-08-20 01:44:46 -04:00
Nicolas Constant 335cbf4956
added reorder state changing 2023-08-20 01:33:07 -04:00
Nicolas Constant b41c31b4ac
better draggable design 2023-08-20 01:16:41 -04:00
Nicolas Constant 41faa36087
working drag&drop 2023-08-20 00:36:52 -04:00
Nicolas Constant 024042959e
fix poll exception on creation 2023-08-19 21:33:43 -04:00
Nicolas Constant f4c87df078
added multi account faq, fix #608 2023-08-19 21:28:20 -04:00
Nicolas Constant d24441343a
always show cw text, fix #480 2023-08-19 20:39:37 -04:00
Nicolas Constant 8c9685045e
added hide boosts, block domain, fix #487 2023-08-19 20:02:37 -04:00
Nicolas Constant a0cb240446
Merge pull request #610 from NicolasConstant/topic_enhance-language-feature
Topic enhance language feature
2023-08-19 16:45:03 -04:00
Nicolas Constant 2def5725f5
option to stop animated avatar, fix #335 2023-08-12 18:41:41 -04:00
Nicolas Constant 450a0088d5
added alt label display option, fix #387 2023-08-12 17:59:34 -04:00
Nicolas Constant d7f988ecb9
added lang autodetect disable option 2023-08-12 17:16:57 -04:00
Nicolas Constant 8703df27d5
display years, fix #603 2023-08-12 17:05:25 -04:00
Nicolas Constant 10fa412173
ensure language is set to provide translation 2023-08-12 16:50:35 -04:00
Nicolas Constant 0b93ed7307
language autodetection functionnal 2023-08-12 01:33:07 -04:00
52 changed files with 2025 additions and 142 deletions

1169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "1.5.0",
"version": "1.7.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
@ -25,13 +25,14 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.7",
"@angular/cdk": "^7.2.7",
"@angular/animations": "^7.2.16",
"@angular/cdk": "^7.3.7",
"@angular/common": "^7.2.7",
"@angular/compiler": "^7.2.7",
"@angular/core": "^7.2.7",
"@angular/forms": "^7.2.7",
"@angular/http": "^7.2.7",
"@angular/material": "^16.2.1",
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/pwa": "^0.12.4",

View File

@ -5,6 +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 { DragDropModule } from '@angular/cdk/drag-drop';
// import { NgxElectronModule } from 'ngx-electron';
import { NgxsModule } from '@ngxs/store';
@ -177,6 +178,7 @@ const routes: Routes = [
OwlDateTimeModule,
OwlNativeDateTimeModule,
OverlayModule,
DragDropModule,
// NgxElectronModule,
RouterModule.forRoot(routes),

View File

@ -15,7 +15,7 @@ import { NavigationService } from '../../services/navigation.service';
import { NotificationService } from '../../services/notification.service';
import { MastodonService } from '../../services/mastodon.service';
import { AuthService } from '../../services/auth.service';
import { SettingsState } from '../../states/settings.state';
describe('CreateStatusComponent', () => {
let component: CreateStatusComponent;
@ -33,7 +33,8 @@ describe('CreateStatusComponent', () => {
NgxsModule.forRoot([
RegisteredAppsState,
AccountsState,
StreamsState
StreamsState,
SettingsState
]),
],
providers: [NavigationService, NotificationService, MastodonService, AuthService],

View File

@ -68,6 +68,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.detectAutosuggestion(value);
this._status = value;
this.languageService.autoDetectLang(value);
setTimeout(() => {
this.autoGrow();
}, 0);
@ -125,6 +127,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
// this.statusStateService.setStatusContent(this.status, this.statusReplyingToWrapper);
// Retrieve mentions
for(let mention of value.status.mentions){
if(this.status){
this.status = this.status.replace(`@${mention.username}`, `@${mention.acct}`);
}
}
this.setVisibilityFromStatus(value.status);
this.title = value.status.spoiler_text;
this.statusLoaded = true;

View File

@ -79,6 +79,8 @@ export class PollEditorComponent implements OnInit {
}
private loadPollParameters(poll: Poll) {
if(!this.oldPoll) return;
const isMulti = poll.multiple;
this.entries.length = 0;

View File

@ -15,7 +15,9 @@
<button type="submit" class="form-button"
title="add account"
[class.comrade__button]="isComrade">
<span *ngIf="!isLoading">Submit</span>
<span *ngIf="!isLoading && !this.isInstanceMultiAccountLoading">Submit</span>
<span *ngIf="!isLoading && this.isInstanceMultiAccountLoading" class="faq__warning">See FAQ</span>
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
</button>
@ -29,5 +31,12 @@
allowfullscreen></iframe>
</div>
<div class="faq" *ngIf="isInstanceMultiAccount">
<p>
FAQ<br/>
<a href="https://github.com/NicolasConstant/sengi/wiki/How-to-add-multiple-accounts-from-the-same-instance" target="_blank">How to add multiple accounts from the same instance?</a>
</p>
</div>
</div>
</div>

View File

@ -109,4 +109,21 @@ $comrade_red: #a50000;
background-color: $comrade_red;
background-position: 0 0;
}
}
.faq {
margin: 20px 0 0 0;
& a {
color: #ffcc00;
text-decoration: underline;
&:hover {
color: #ffe88a;
}
}
&__warning {
color: #ffdc52;
}
}

View File

@ -6,13 +6,14 @@ import { RegisteredAppsStateModel, AppInfo, AddRegisteredApp } from '../../../st
import { AuthService, CurrentAuthProcess } from '../../../services/auth.service';
import { AppData } from '../../../services/models/mastodon.interfaces';
import { NotificationService } from '../../../services/notification.service';
import { ToolsService } from '../../../services/tools.service';
@Component({
selector: 'app-add-new-account',
templateUrl: './add-new-account.component.html',
styleUrls: ['./add-new-account.component.scss']
})
export class AddNewAccountComponent implements OnInit {
export class AddNewAccountComponent implements OnInit {
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
private comradeList = ['juche.town'];
@ -24,12 +25,14 @@ export class AddNewAccountComponent implements OnInit {
set setInstance(value: string) {
this.instance = value.replace('http://', '').replace('https://', '').replace('/', '').toLowerCase().trim();
this.checkComrad();
this.checkInstanceMultiAccount(value);
}
get setInstance(): string {
return this.instance;
}
constructor(
private readonly toolsService: ToolsService,
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store) { }
@ -51,8 +54,27 @@ export class AddNewAccountComponent implements OnInit {
this.isComrade = false;
}
isInstanceMultiAccount: boolean;
isInstanceMultiAccountLoading: boolean;
checkInstanceMultiAccount(value: string) {
if(value) {
const instances: string[] = this.toolsService.getAllAccounts().map(x => x.instance);
if(instances && instances.indexOf(value) > -1){
this.isInstanceMultiAccount = true;
this.isInstanceMultiAccountLoading = true;
setTimeout(() => {
this.isInstanceMultiAccountLoading = false;
}, 2000);
} else {
this.isInstanceMultiAccount = false;
this.isInstanceMultiAccountLoading = false;
}
}
}
onSubmit(): boolean {
if(this.isLoading || !this.instance) return false;
if(this.isLoading || !this.instance || this.isInstanceMultiAccountLoading) return false;
this.isLoading = true;

View File

@ -10,7 +10,7 @@
<div class="floating-column__inner--left">
<div class="floating-column__header">
<a class="close-button" href (click)="closePanel()" title="close">
<fa-icon [icon]="faTimes"></fa-icon>
<fa-icon class="close-button__icon" [icon]="faTimes"></fa-icon>
</a>
</div>

View File

@ -29,9 +29,20 @@
}
.close-button {
// outline: 1px dotted orange;
display: block;
float: right;
font-size: 14px;
color: white;
margin: 10px 16px 0 0;
margin: 5px 5px 0 0;
width: 40px;
height: 34px;
&__icon {
position: relative;
top: 6px;
left: 17px;
}
}

View File

@ -122,6 +122,17 @@ export class MyAccountComponent implements OnInit, OnDestroy {
}
}
})
.then(_ => {
this.availableLists.sort((a,b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
})
.catch(err => {
this.notificationService.notifyHttpError(err, this.account.info);
});

View File

@ -70,17 +70,29 @@
</div>
<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>
[statusWrapper]="notification.status"
[notificationAccount]="notification.account"
[notificationType]="notification.type"
[context]="'notifications'"
(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>
[statusWrapper]="notification.status"
[context]="'notifications'"
(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>
class="stream__status"
[statusWrapper]="notification.status"
[notificationAccount]="notification.account"
[notificationType]="notification.type"
[context]="'notifications'"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>

View File

@ -62,12 +62,20 @@
<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"/>
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>
<input class="sub-section__checkbox" [(ngModel)]="disableLangAutodetectEnabled"
(change)="onDisableLangAutodetectChanged()" type="checkbox" name="disableLangAutodetec"
value="disableLangAutodetec" id="disableLangAutodetec">
<label class="noselect sub-section__label" for="disableLangAutodetec">disable language autodetection</label>
</div>
<h4 class="panel__subtitle">Twitter Bridge</h4>
<div class="sub-section">
@ -159,7 +167,8 @@
<input class="sub-section__checkbox" [checked]="timeLineHeader === 6" (change)="onTimeLineHeaderChange(6)"
type="radio" name="timelineheader-6" value="timelineheader-6" id="timelineheader-6">
<label class="noselect sub-section__label" for="timelineheader-6">Title - Account Icon - Username - Domain Name</label>
<label class="noselect sub-section__label" for="timelineheader-6">Title - Account Icon - Username - Domain
Name</label>
<br>
<span class="sub-section__title">loading behavior:</span><br />
@ -186,7 +195,8 @@
<input class="sub-section__checkbox" [(ngModel)]="autoFollowOnListEnabled"
(change)="onAutoFollowOnListChanged()" type="checkbox" name="onAutoFollowOnListChanged"
value="onAutoFollowOnListChanged" id="onAutoFollowOnListChanged">
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when adding to list</label>
<label class="noselect sub-section__label" for="onAutoFollowOnListChanged">autofollow accounts when
adding to list</label>
<br>
</div>
</div>
@ -199,6 +209,20 @@
<label class="noselect sub-section__label" for="disableRemoteFetching">disable remote status
fetching</label>
<br>
<input class="sub-section__checkbox" [(ngModel)]="enableAltLabelEnabled"
(change)="onEnableAltLabelChanged()" type="checkbox" name="enableAltLabel"
value="enableAltLabel" id="enableAltLabel">
<label class="noselect sub-section__label" for="enableAltLabel">enable alt label</label>
<br>
<input class="sub-section__checkbox" [(ngModel)]="enableFreezeAvatarEnabled"
(change)="onEnableFreezeAvatarChanged()" type="checkbox" name="enableFreezeAvatar"
value="enableFreezeAvatar" id="enableFreezeAvatar">
<label class="noselect sub-section__label" for="enableFreezeAvatar">freeze animated avatar</label>
<br>
reorder account's icons: <a href class="toogle-lock-icon-menu" (click)="toogleLockIconMenu()"><span *ngIf="iconMenuLocked">Unlock Icons</span><span *ngIf="!iconMenuLocked">Lock Icons</span></a>
</div>
<h4 class="panel__subtitle">About</h4>

View File

@ -153,4 +153,22 @@
background-color: #32384d;
}
}
}
.toogle-lock-icon-menu {
display: block;
padding: 3px 40px;
width: 170px;
float: right;
text-align: center;
color: white;
background-color: #1f2330;
&:hover {
text-decoration: none;
background-color: #32384d;
}
}

View File

@ -29,6 +29,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
disableRemoteStatusFetchingEnabled: boolean;
disableAvatarNotificationsEnabled: boolean;
disableSoundsEnabled: boolean;
disableLangAutodetectEnabled: boolean;
enableAltLabelEnabled: boolean;
enableFreezeAvatarEnabled: boolean;
version: string;
hasPleromaAccount: boolean;
@ -146,11 +149,21 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.twitterBridgeInstance = settings.twitterBridgeInstance;
this.configuredLangs = this.languageService.getConfiguredLanguages();
this.disableLangAutodetectEnabled = settings.disableLangAutodetec;
this.enableAltLabelEnabled = settings.enableAltLabel;
this.enableFreezeAvatarEnabled = settings.enableFreezeAvatar;
}
ngOnDestroy(): void {
if(this.languageSub) this.languageSub.unsubscribe();
}
}
iconMenuLocked = true;
toogleLockIconMenu(): boolean {
this.navigationService.changeIconMenuState(this.iconMenuLocked);
this.iconMenuLocked = ! this.iconMenuLocked;
return false;
}
onSearchLang(input: string) {
this.searchedLangs = this.languageService.searchLanguage(input);
@ -273,6 +286,27 @@ export class SettingsComponent implements OnInit, OnDestroy {
return false;
}
onEnableFreezeAvatarChanged(){
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.enableFreezeAvatar = this.enableFreezeAvatarEnabled;
this.settingsService.saveSettings(settings);
}
onEnableAltLabelChanged(){
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.enableAltLabel = this.enableAltLabelEnabled;
this.settingsService.saveSettings(settings);
}
onDisableLangAutodetectChanged() {
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();
settings.disableLangAutodetec = this.disableLangAutodetectEnabled;
this.settingsService.saveSettings(settings);
}
onDisableAutofocusChanged() {
this.notifyRestartNeeded();
let settings = this.settingsService.getSettings();

View File

@ -8,27 +8,36 @@
<fa-icon [icon]="faSearch"></fa-icon>
</a>
<div *ngFor="let account of accounts">
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
(openMenuNotify)="onOpenMenuNotify($event)">
</app-account-icon>
<div *ngIf="!iconMenuIsDraggable">
<div *ngFor="let account of accounts">
<app-account-icon [account]="account" (toogleAccountNotify)="onToogleAccountNotify($event)"
(openMenuNotify)="onOpenMenuNotify($event)">
</app-account-icon>
</div>
</div>
<div *ngIf="iconMenuIsDraggable" cdkDropList [cdkDropListData]="accounts" (cdkDropListDropped)="onDrop($event)">
<div *ngFor="let account of accounts" cdkDrag class="draggable">
<fa-icon class="draggable__icon" [icon]="faArrowsAltV"></fa-icon>
<img class="draggable__avatar" src="{{ account.avatar }}" />
</div>
</div>
<a class="left-bar-button left-bar-button--add left-bar-link" [ngClass]="{'no-accounts': hasAccounts === false }"
href title="add new account" (click)="addNewAccount()" (contextmenu)="addNewAccount()">
<fa-icon [icon]="faPlus"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href title="scheduled statuses"
*ngIf="hasAccounts && hasScheduledStatuses"
(click)="openScheduledStatuses()"
<a class="left-bar-button left-bar-button--scheduled left-bar-button--bottom left-bar-link" href
title="scheduled statuses" *ngIf="hasAccounts && hasScheduledStatuses" (click)="openScheduledStatuses()"
(contextmenu)="openScheduledStatuses()">
<fa-icon [icon]="faCalendarAlt"></fa-icon>
</a>
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings" (click)="openSettings()"
(contextmenu)="openSettings()">
<a class="left-bar-button left-bar-button--cog left-bar-button--bottom left-bar-link" href title="settings"
(click)="openSettings()" (contextmenu)="openSettings()">
<fa-icon [icon]="faCog"></fa-icon>
</a>
</div>

View File

@ -82,4 +82,38 @@ $height-button: 40px;
.no-accounts {
padding-top: 10px;
// color: cornflowerblue;
}
$draggable-accent-color: #47e927;
// $draggable-accent-color: #a8ff97;
.draggable {
width: 40px;
height: 40px;
margin: auto;
margin-bottom: 5px;
border: 2px solid #df0adf;
border: 2px solid $draggable-accent-color;
border-radius: 2px;
position: relative;
&__avatar {
width: calc(100%);
opacity: .8;
}
&__icon {
position: absolute;
float: left;
z-index: 5;
color:$draggable-accent-color;
top: 6px;
left: 12px;
font-size: 18px;
}
}

View File

@ -1,12 +1,13 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Subscription, Observable } from "rxjs";
import { Store } from "@ngxs/store";
import { faPlus, faCog, faSearch } from "@fortawesome/free-solid-svg-icons";
import { faPlus, faCog, faSearch, faArrowsAltV } from "@fortawesome/free-solid-svg-icons";
import { faCommentAlt, faCalendarAlt } from "@fortawesome/free-regular-svg-icons";
import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { AccountWrapper } from "../../models/account.models";
import { AccountInfo, SelectAccount } from "../../states/accounts.state";
import { AccountInfo, ReorderAccounts, SelectAccount } from "../../states/accounts.state";
import { NavigationService, LeftPanelType } from "../../services/navigation.service";
import { UserNotificationService, UserNotification } from '../../services/user-notification.service';
import { ToolsService } from '../../services/tools.service';
@ -24,6 +25,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
faPlus = faPlus;
faCog = faCog;
faCalendarAlt = faCalendarAlt;
faArrowsAltV = faArrowsAltV;
accounts: AccountWithNotificationWrapper[] = [];
hasAccounts: boolean;
@ -33,6 +35,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
private accountSub: Subscription;
private scheduledSub: Subscription;
private notificationSub: Subscription;
private draggableIconMenuSub: Subscription;
constructor(
private readonly settingsService: SettingsService,
@ -103,7 +106,13 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
}
}
iconMenuIsDraggable = false;
ngOnInit() {
this.draggableIconMenuSub = this.navigationService.enableDraggableIconMenu.subscribe(x => {
this.iconMenuIsDraggable = x;
});
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (accounts) {
//Update and Add
@ -164,6 +173,17 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
this.accountSub.unsubscribe();
this.notificationSub.unsubscribe();
this.scheduledSub.unsubscribe();
this.draggableIconMenuSub.unsubscribe();
}
onDrop(event: CdkDragDrop<AccountWithNotificationWrapper[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data,
event.previousIndex,
event.currentIndex);
this.store.dispatch([new ReorderAccounts(this.accounts.map(x => x.info))])
}
}
onToogleAccountNotify(acc: AccountWrapper) {

View File

@ -27,7 +27,13 @@
<ng-template contextMenuItem (execute)="unmuteConversation()" *ngIf="statusWrapper && isOwnerSelected && displayedStatus.muted">
Unmute conversation
</ng-template>
<ng-template contextMenuItem divider="true"></ng-template>
<ng-template contextMenuItem (execute)="hideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && this.relationship.showing_reblogs">
Hide boosts from @{{ this.username }}
</ng-template>
<ng-template contextMenuItem (execute)="unhideBoosts()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.following && !this.relationship.showing_reblogs">
Unhide boosts from @{{ this.username }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="muteAccount()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.muting">
Mute @{{ this.username }}
</ng-template>
@ -40,6 +46,14 @@
<ng-template contextMenuItem (execute)="unblockAccount()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.blocking">
Unblock @{{ this.username }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="!isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="blockDomain()" *ngIf="!isOwnerSelected && this.relationship && !this.relationship.domain_blocking">
Block domain {{ this.domain }}
</ng-template>
<ng-template contextMenuItem (execute)="unblockDomain()" *ngIf="!isOwnerSelected && this.relationship && this.relationship.domain_blocking">
Unblock domain {{ this.domain }}
</ng-template>
<ng-template contextMenuItem divider="true" *ngIf="isOwnerSelected"></ng-template>
<ng-template contextMenuItem (execute)="pinOnProfile()" *ngIf="statusWrapper && isOwnerSelected && !displayedStatus.pinned && displayedStatus.visibility === 'public'">
Pin on profile
</ng-template>

View File

@ -25,6 +25,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
private loadedAccounts: AccountInfo[];
displayedStatus: Status;
username: string;
domain: string;
isOwnerSelected: boolean;
isEditingAvailable: boolean;
@ -74,6 +75,7 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
}
this.username = account.acct.split('@')[0];
this.domain = account.acct.split('@')[1];
this.fullHandle = this.toolsService.getAccountFullHandle(account);
}
@ -167,6 +169,38 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
return false;
}
hideBoosts(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.hideBoosts(acc, target);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
unhideBoosts(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(acc, this.fullHandle)
.then(async (target: Account) => {
const relationship = await this.mastodonService.unhideBoosts(acc, target);
this.relationship = relationship;
this.relationshipChanged.next(relationship);
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
muteAccount(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
@ -241,6 +275,37 @@ export class StatusUserContextMenuComponent implements OnInit, OnDestroy {
return false;
}
blockDomain(): boolean {
const response = confirm(`Are you really sure you want to block the entire ${this.domain} domain? You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.`);
if (response) {
const acc = this.toolsService.getSelectedAccounts()[0];
this.mastodonService.blockDomain(acc, this.domain)
.then(_ => {
this.relationship.domain_blocking = true;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
}
return false;
}
unblockDomain(): boolean {
const acc = this.toolsService.getSelectedAccounts()[0];
this.mastodonService.blockDomain(acc, this.domain)
.then(_ => {
this.relationship.domain_blocking = false;
})
.catch(err => {
this.notificationService.notifyHttpError(err, acc);
});
return false;
}
muteConversation(): boolean {
const selectedAccount = this.toolsService.getSelectedAccounts()[0];

View File

@ -1,4 +1,8 @@
<div class="image">
<div class="image">
<div class="image__alt" *ngIf="displayAltLabel && attachment.description" title="{{ attachment.description }}">ALT</div>
<a *ngIf="status" href class="image__status" (click)="openStatus()" (auxclick)="openStatus()" title="open status">
<fa-icon class="image__status--icon" [icon]="faExternalLinkAlt"></fa-icon>
</a>
<a href class="image__link" (click)="openExternal()" (auxclick)="openExternal()" title="open image">
<fa-icon class="image__link--icon" [icon]="faLink"></fa-icon>
</a>

View File

@ -25,10 +25,48 @@
// }
}
&__status {
z-index: 10;
position: absolute;
top: 0;
right: 25px;
padding: 5px 5px 8px 8px;
transition: all .2s;
opacity: 0;
color: white;
&--icon {
filter: drop-shadow(0 0 3px rgb(78, 78, 78));
}
}
&:hover &__link {
opacity: 1;
cursor: pointer;
}
&:hover &__status {
opacity: 1;
cursor: pointer;
}
&__alt {
display: inline;
color: white;
z-index: 10;
position: absolute;
bottom: 5px;
left: 5px;
font-size: 10px;
font-weight: bolder;
background-color: rgba($color: #000000, $alpha: 0.5);
border-radius: 3px;
padding: 2px 5px;
}
}
img,

View File

@ -1,7 +1,10 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { faLink, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { SettingsService } from '../../../../../services/settings.service';
import { Attachment } from '../../../../../services/models/mastodon.interfaces';
import { StatusWrapper } from '../../../../../models/common.model';
import { OpenThreadEvent } from '../../../../../services/tools.service';
@Component({
selector: 'app-attachement-image',
@ -10,11 +13,19 @@ import { Attachment } from '../../../../../services/models/mastodon.interfaces';
})
export class AttachementImageComponent implements OnInit {
faLink = faLink;
faExternalLinkAlt = faExternalLinkAlt;
displayAltLabel: boolean;
@Input() attachment: Attachment;
@Input() status: StatusWrapper;
@Output() openEvent = new EventEmitter();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
constructor() { }
constructor(
private readonly settingsService: SettingsService
) {
this.displayAltLabel = this.settingsService.getSettings().enableAltLabel;
}
ngOnInit() {
}
@ -28,4 +39,13 @@ export class AttachementImageComponent implements OnInit {
window.open(this.attachment.url, '_blank');
return false;
}
openStatus(): boolean {
if(!this.status) return false;
const openThreadEvent = new OpenThreadEvent(this.status.status, this.status.provider);
this.browseThreadEvent.next(openThreadEvent);
return false;
}
}

View File

@ -202,7 +202,7 @@ describe('DatabindedTextComponent', () => {
const sample = `<p>Bla <a href="https://ubuntu.social/tags/kubecon" rel="tag">#<span>KubeCon</span></a> Bla</p>`;
component.text = sample;
expect(component.processedText).toContain('<p>Bla <a href="https://ubuntu.social/tags/kubecon" class="hashtag-KubeCon" title="#KubeCon" target="_blank" rel="noopener noreferrer">#KubeCon</a> Bla</p>');
expect(component.processedText).toContain('<p>Bla <a href="https://ubuntu.social/tags/kubecon" class="hashtag-KubeCon" title="#KubeCon" target="_blank" rel="noopener noreferrer">#KubeCon</a> Bla</p>');
});
it('should parse link - Pleroma', () => {

View File

@ -45,7 +45,15 @@ export class PollComponent implements OnInit {
}
this.options.length = 0;
const maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
let maxVotes = Math.max(...this.poll.options.map(x => x.votes_count));
if(!this.poll.multiple){ //Fix for absurd values in pleroma
this.poll.voters_count = this.poll.votes_count;
} else if(this.poll.voters_count * this.poll.options.length < this.poll.votes_count){
this.poll.voters_count = this.poll.votes_count;
}
let i = 0;
for (let opt of this.poll.options) {
let optWrapper = new PollOptionWrapper(i, opt, this.poll.votes_count, this.poll.voters_count, opt.votes_count === maxVotes);
@ -195,7 +203,7 @@ class PollOptionWrapper implements PollOption {
if (totalVotes === 0) {
this.percentage = '0';
} else {
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
this.percentage = ((this.votes_count / votesDivider) * 100).toFixed(0);
}
this.isMax = isMax;
}

View File

@ -64,6 +64,7 @@ export class StatusTranslateComponent implements OnInit, OnDestroy {
.then(canTranslate => {
if (canTranslate
&& !this.status.isRemote
&& this.status.status.language
&& this.configuredLanguages.length > 0
&& this.configuredLanguages.findIndex(x => x.iso639 === this.status.status.language) === -1) {

View File

@ -2,7 +2,7 @@
<div class="reblog" *ngIf="reblog">
<a class="reblog__profile-link" href title="{{ status.account.acct }}"
(click)="openAccount(status.account)"
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar | ensureHttps }}" /></a> boosted
(auxclick)="openUrl(status.account.url)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ getAvatar(status.account) | ensureHttps }}" /></a> boosted
</div>
<div *ngIf="statusWrapper.status.pinned && !notificationType" class="pinned">
<div class="notification--icon">
@ -60,9 +60,9 @@
<div [ngClass]="{'notification--status': notificationAccount }">
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}"
(click)="openAccount(displayedStatus.account)" (auxclick)="openUrl(displayedStatus.account.url)">
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ displayedStatus.account.avatar | ensureHttps }}" />
<img [class.status__avatar--boosted]="reblog || notificationAccount" class="status__avatar" src="{{ getAvatar(displayedStatus.account) | ensureHttps }}" />
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ notificationAccount.avatar | ensureHttps }}" />
<img *ngIf="notificationAccount" class="notification--avatar" src="{{ getAvatar(notificationAccount) | ensureHttps }}" />
<span class="status__name">
<span class="status__name--displayname"
innerHTML="{{displayedStatus.account | accountEmoji}}"></span><span
@ -109,6 +109,11 @@
<span class="status__content-warning--title">sensitive content</span>
<span innerHTML="{{ contentWarningText }}"></span>
</a>
<div class="status__content-warning__closed" *ngIf="!isContentWarned && contentWarningText" title="content warning">
<span innerHTML="{{ contentWarningText }}"></span>
</div>
<app-databinded-text #databindedtext class="status__content" *ngIf="!isContentWarned" [text]="statusContent" [selected]="isSelected"
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
(textSelected)="textSelected()"></app-databinded-text>

View File

@ -172,6 +172,26 @@
border: 3px solid $status-secondary-color;
color: whitesmoke;
&__closed {
//margin: 0 5px 0 $avatar-column-space;
margin: 0 5px 0 calc(#{$avatar-column-space} - 1px);
padding: 3px 5px 3px 5px;
margin-bottom: 5px;
overflow-wrap: break-word;
font-size: 12px;
border-radius: 4px;
// color: #6d8fd3;
// color: #7282a1;
// color: #838da1;
color: #919bb1;
// background-color: #273149;
// background-color: #1f273a;
background-color: #171d2b;
}
&--title {
color: $content-warning-font-color;
font-size: 11px;

View File

@ -10,6 +10,7 @@ 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";
import { SettingsService } from "../../../services/settings.service";
@Component({
selector: "app-status",
@ -44,6 +45,8 @@ export class StatusComponent implements OnInit {
isSelected: boolean;
isRemote: boolean;
private freezeAvatarEnabled: boolean;
hideStatus: boolean = false;
@Output() browseAccountEvent = new EventEmitter<string>();
@ -56,6 +59,8 @@ export class StatusComponent implements OnInit {
@Input() notificationType: 'mention' | 'reblog' | 'favourite' | 'poll' | 'update';
@Input() notificationAccount: Account;
@Input() context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
private _statusWrapper: StatusWrapper;
status: Status;
@ -95,6 +100,8 @@ export class StatusComponent implements OnInit {
// this.statusAccountName = this.emojiConverter.applyEmojis(this.displayedStatus.account.emojis, this.displayedStatus.account.display_name, EmojiTypeEnum.small);
let statusContent = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, this.displayedStatus.content, EmojiTypeEnum.medium);
this.statusContent = this.ensureMentionAreDisplayed(statusContent);
this.validateFilteringStatus();
}
get statusWrapper(): StatusWrapper {
return this._statusWrapper;
@ -103,6 +110,7 @@ export class StatusComponent implements OnInit {
constructor(
public elem: ElementRef,
private readonly toolsService: ToolsService,
private readonly settingsService: SettingsService,
private readonly statusesStateService: StatusesStateService) { }
ngOnInit() {
@ -111,12 +119,52 @@ export class StatusComponent implements OnInit {
this.statusWrapper = notification.editedStatus;
}
});
this.freezeAvatarEnabled = this.settingsService.getSettings().enableFreezeAvatar;
}
ngOnDestroy() {
if (this.statusesStateServiceSub) this.statusesStateServiceSub.unsubscribe();
}
private validateFilteringStatus(){
const filterStatus = this.displayedStatus.filtered;
if(!filterStatus || filterStatus.length === 0) return;
// if(!this.context){
// console.warn('this.context not found');
// console.warn(this.context);
// }
for (let filter of filterStatus) {
if(this.context && filter.filter.context && filter.filter.context.length > 0){
if(!filter.filter.context.includes(this.context)) continue;
}
if(filter.filter.filter_action === 'warn'){
this.isContentWarned = true;
let filterTxt = `FILTERED:`;
for(let w of filter.keyword_matches){
filterTxt += ` ${w}`;
}
this.contentWarningText = filterTxt;
} else if (filter.filter.filter_action === 'hide'){
this.hideStatus = true;
}
}
}
getAvatar(acc: Account): string {
if(this.freezeAvatarEnabled){
return acc.avatar_static;
} else {
return acc.avatar;
}
}
private ensureMentionAreDisplayed(data: string): string {
const mentions = this.displayedStatus.mentions;
if (!mentions || mentions.length === 0) return data;

View File

@ -122,7 +122,7 @@ export class StreamNotificationsComponent extends BrowseBase {
loadNotifications(): any {
this.account = this.toolsService.getAccountById(this.streamElement.accountId);
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.mentionsSubscription = this.userNotificationService.userNotifications.subscribe((userNotifications: UserNotification[]) => {
this.loadMentions(userNotifications);
});
@ -130,10 +130,13 @@ export class StreamNotificationsComponent extends BrowseBase {
.then((notifications: Notification[]) => {
this.isNotificationsLoading = false;
this.notifications = notifications.map(x => {
let wrappedNotification= notifications.map(x => {
let cwPolicy = this.toolsService.checkContentWarning(x.status);
return new NotificationWrapper(x, this.account, cwPolicy.applyCw, cwPolicy.hide);
});
this.notifications = wrappedNotification.filter(x => x.type !== 'mention' || (x.type === 'mention' && x.status.status !== null));
this.lastNotificationId = this.notifications[this.notifications.length - 1].notification.id;
})
.catch(err => {

View File

@ -1,9 +1,5 @@
<div class="overlay">
<div class="overlay__header">
<a href class="overlay__button overlay-close" title="close" (click)="close()">
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
</a>
<a href class="overlay__button overlay-previous"
[ngClass]="{'overlay__button--focus': hasPreviousElements }" title="previous" (click)="previous()">
<fa-icon class="overlay-previous__icon" [icon]="faAngleLeft"></fa-icon>
@ -12,13 +8,17 @@
title="refresh" (click)="refresh()">
<fa-icon class="overlay-refresh__icon" [icon]="faRedoAlt"></fa-icon>
</a>
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
title="next" (click)="next()">
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
</a>
<a href title="return to top" class="overlay-gototop" (click)="goToTop()">
</a>
<a href class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': hasNextElements }"
title="next" (click)="next()">
<fa-icon class="overlay-next__icon" [icon]="faAngleRight"></fa-icon>
<a href class="overlay__button overlay-close" title="close" (click)="close()">
<fa-icon class="overlay-close__icon" [icon]="faTimes"></fa-icon>
</a>
</div>

View File

@ -11,7 +11,9 @@ $header-content-height: 40px;
width: calc(100%);
height: $header-content-height;
background-color: $column-header-background-color;
border-bottom: 1px solid #222736;
border-bottom: 1px solid #222736;
display: flex;
}
&__content-wrapper {
transition: all .2s;
@ -44,11 +46,17 @@ $header-content-height: 40px;
}
&__button {
// outline: 1px dotted orange;
width: 25px;
height: 25px;
width: $header-content-height;
height: $header-content-height;
color: #354060;
transition: all .2s;
margin: 8px 0 0 8px;
&:hover {
color: #536599;
color: #7a8dc7;
@ -68,19 +76,8 @@ $header-content-height: 40px;
&__icon {
position: relative;
left: 7px;
top: -1px
}
}
&-next {
display: block;
float: left;
font-size: 18px;
&__icon {
position: relative;
left: 8px;
top: -1px
left: 17px;
top: 7px
}
}
&-refresh {
@ -90,29 +87,38 @@ $header-content-height: 40px;
&__icon {
position: relative;
left: 5px;
top: 1px
left: 13px;
top: 9px
}
}
&-next {
display: block;
float: left;
font-size: 18px;
&__icon {
position: relative;
left: 13px;
top: 7px
}
}
&-gototop {
position: absolute;
top: 0;
left: 110px;
right: 40px;
// outline: 1px dotted orange;
flex-grow: 1;
display: block;
height: $header-content-height;
}
&-close {
display: block;
float: right;
font-size: 13px;
color: white;
margin-right: 8px;
&__icon {
position: relative;
left: 7px;
top: 1px
left: 15px;
top: 9px
}
}
}

View File

@ -17,8 +17,11 @@
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses" #status>
<app-status
[statusWrapper]="statusWrapper" [isThreadDisplay]="isThread"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
[statusWrapper]="statusWrapper"
[isThreadDisplay]="isThread"
[context]="context"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
</div>

View File

@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http';
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { StreamElement } from '../../../states/streams.state';
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
import { AccountInfo } from '../../../states/accounts.state';
import { StreamingService, EventEnum, StatusUpdate } from '../../../services/streaming.service';
import { Status } from '../../../services/models/mastodon.interfaces';
@ -20,9 +20,11 @@ import { SettingsService } from '../../../services/settings.service';
templateUrl: './stream-statuses.component.html',
styleUrls: ['./stream-statuses.component.scss']
})
export class StreamStatusesComponent extends TimelineBase {
export class StreamStatusesComponent extends TimelineBase {
protected _streamElement: StreamElement;
context: 'home' | 'notifications' | 'public' | 'thread' | 'account';
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
@ -32,6 +34,8 @@ export class StreamStatusesComponent extends TimelineBase {
this.hideReplies = streamElement.hideReplies;
this.load(this._streamElement);
this.setContext(this._streamElement);
}
get streamElement(): StreamElement {
return this._streamElement;
@ -112,6 +116,24 @@ export class StreamStatusesComponent extends TimelineBase {
if (this.deleteStatusSubscription) this.deleteStatusSubscription.unsubscribe();
}
private setContext(streamElement: StreamElement) {
switch(streamElement.type){
case StreamTypeEnum.global:
case StreamTypeEnum.local:
case StreamTypeEnum.tag:
this.context = 'public';
break;
case StreamTypeEnum.personnal:
case StreamTypeEnum.list:
this.context = 'home';
break;
case StreamTypeEnum.activity:
case StreamTypeEnum.directmessages:
this.context = 'notifications';
break;
}
}
refresh(): any {
this.load(this._streamElement);
}

View File

@ -28,6 +28,8 @@ export class ThreadComponent extends BrowseBase {
hasContentWarnings = false;
private remoteStatusFetchingDisabled = false;
context = 'thread';
numNewItems: number; //html compatibility only
bufferStream: Status[] = []; //html compatibility only
streamPositionnedAtTop: boolean = true; //html compatibility only

View File

@ -166,39 +166,27 @@
<div class="profile__extra-info profile__extra-info__preparefloating" *ngIf="!isLoading"
[class.profile__extra-info__floating]="showFloatingStatusMenu">
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')" title="Status"
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
</div>
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
title="Status & Replies"
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
Replies</a>
</div>
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
</div>
</div>
<div class="profile-statuses" #profilestatuses>
<div class="profile__extra-info" *ngIf="!isLoading">
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('status')"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('status')"
title="Status"
[class.profile__extra-info__links--selected]="statusSection === 'status'">Status</a>
</div>
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('replies')"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('replies')"
title="Status & Replies"
[class.profile__extra-info__links--selected]="statusSection === 'replies'">Status &
Replies</a>
</div>
<div class="profile__extra-info__section">
<a href class="profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
<a href class="profile__extra-info__section profile__extra-info__links" (click)="switchStatusSection('media')" title="Media"
[class.profile__extra-info__links--selected]="statusSection === 'media'">Media</a>
</div>
</div>
<div [class.profile__status-switching-section]="isSwitchingSection">
@ -208,21 +196,29 @@
<div *ngIf="statusSection === 'status' && !statusLoading">
<div *ngFor="let statusWrapper of pinnedStatuses">
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
<app-status
[statusWrapper]="statusWrapper"
[context]="'account'"
(browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseThreadEvent)="browseThread($event)">
</app-status>
</div>
</div>
<div *ngFor="let statusWrapper of statuses">
<div *ngIf="statusSection !== 'media'">
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
<app-status
[statusWrapper]="statusWrapper"
[context]="'account'"
(browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseThreadEvent)="browseThread($event)">
</app-status>
</div>
<div *ngIf="statusSection === 'media'" class="status-media">
<div *ngFor="let media of statusWrapper.status.media_attachments">
<app-attachement-image *ngIf="media.type === 'image' || media.type === 'gifv'" class="status-media__image" [attachment]="media" (openEvent)="openAttachment(media)"></app-attachement-image>
<app-attachement-image *ngIf="media.type === 'image' || media.type === 'gifv'" class="status-media__image" [attachment]="media" [status]="statusWrapper" (openEvent)="openAttachment(media)" (browseThreadEvent)="browseThread($event)"></app-attachement-image>
</div>
</div>
</div>

View File

@ -275,14 +275,15 @@ $floating-header-height: 60px;
&-follows {
width: calc(100%);
font-size: 13px;
border-bottom: 1px solid #0f111a;;
border-bottom: 1px solid #0f111a;
display: flex;
&__link {
color: white;
width: calc(50%);
flex-grow: 1;
padding: 5px;
text-align: center;
display: inline-block;
background-color: #1a1f2e;
transition: all .2s;
@ -311,15 +312,15 @@ $floating-header-height: 60px;
font-size: 13px;
transition: all .4s;
&__section {
text-align: center;
display: inline-block;
width: calc(33.333% - 5px);
padding: 5px 0 7px 0;
display: flex;
&:not(:last-child) {
margin-right: 5px;
}
&__section {
// outline: 1px dotted orange;
flex-grow: 1;
text-align: center;
padding: 5px 0 7px 0;
}
&__preparefloating {

View File

@ -7,7 +7,7 @@ import { Store } from '@ngxs/store';
import { Account, Status, Relationship, Attachment } from "../../../services/models/mastodon.interfaces";
import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { ToolsService, OpenThreadEvent, InstanceType } from '../../../services/tools.service';
import { NotificationService } from '../../../services/notification.service';
import { AccountInfo } from '../../../states/accounts.state';
import { StatusWrapper, OpenMediaEvent } from '../../../models/common.model';
@ -286,21 +286,44 @@ export class UserProfileComponent extends BrowseBase {
}
follow(): boolean {
this.loadingRelationShip = true;
const userAccount = this.toolsService.getSelectedAccounts()[0];
let foundAccountToFollow: Account;
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
foundAccountToFollow = account;
return this.mastodonService.follow(userAccount, account);
})
.then((relationship: Relationship) => {
this.relationship = relationship;
this.relationship = relationship;
})
.then(async () => {
// Double check for pleroma users
const instanceInfo = await this.toolsService.getInstanceInfo(userAccount);
if(instanceInfo.type === InstanceType.Pleroma || instanceInfo.type === InstanceType.Akkoma){
await new Promise(resolve => setTimeout(resolve, 1000))
const relationships = await this.mastodonService.getRelationships(userAccount, [foundAccountToFollow]);
const relationship = relationships.find(x => x.id === foundAccountToFollow.id);
if(relationship){
this.relationship = relationship;
}
}
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.loadingRelationShip = false;
});
return false;
}
unfollow(): boolean {
this.loadingRelationShip = true;
const userAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(userAccount, this.lastAccountName)
.then((account: Account) => {
@ -311,6 +334,9 @@ export class UserProfileComponent extends BrowseBase {
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, userAccount);
})
.then(() => {
this.loadingRelationShip = false;
});
return false;
}

View File

@ -30,7 +30,7 @@ export class TimeAgoPipe implements PipeTransform {
const hours = minutes / 60;
const days = hours / 24;
// const months = days / 30.416;
// const years = days / 365;
const years = days / 365;
if (seconds <= 59) {
text = Math.round(seconds) + 's';
@ -38,8 +38,10 @@ export class TimeAgoPipe implements PipeTransform {
text = Math.round(minutes) + 'm';
} else if (hours <= 23) {
text = Math.round(hours) + 'h';
} else {
} else if (days < 365) {
text = Math.round(days) + 'd';
} else {
text = Math.round(years) + 'y';
}
if (minutes < 1) {

View File

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

View File

@ -1,10 +1,30 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class MyElectronService {
detectedLangSubject = new Subject<DetectedLang[]>();
constructor() {
try {
if ((<any>window).api) {
(<any>window).api.receive("detectedLang", (data) => {
const result = [];
for (const l of data) {
let newLang = new DetectedLang(l[0], l[1]);
result.push(newLang);
}
this.detectedLangSubject.next(result);
});
}
}
catch (err) {
console.error(err);
}
this.detectLang("ceci est une phrase");
}
setLang(lang: string) {
@ -17,4 +37,22 @@ export class MyElectronService {
console.error(err);
}
}
detectLang(text: string) {
try {
if ((<any>window).api) {
(<any>window).api.send("detectLang", text);
}
}
catch (err) {
console.error(err);
}
}
}
export class DetectedLang {
constructor(
public lang: string,
public score: number
) {}
}

View File

@ -31,6 +31,8 @@ export class InstancesInfoService {
const instanceV1 = <Instancev1>instance;
if (instanceV1 && instanceV1.max_toot_chars)
return instanceV1.max_toot_chars;
if(instanceV1 && instanceV1.configuration && instanceV1.configuration.statuses && instanceV1.configuration.statuses.max_characters)
return instanceV1.configuration.statuses.max_characters;
}
return this.defaultMaxChars;

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ILanguage } from '../states/settings.state';
import { MyElectronService } from './electron.service';
import { DetectedLang, MyElectronService } from './electron.service';
import { SettingsService } from './settings.service';
@Injectable({
@ -19,6 +19,42 @@ export class LanguageService {
) {
this.configuredLanguagesChanged.next(this.getConfiguredLanguages());
this.selectedLanguageChanged.next(this.getSelectedLanguage());
this.electronService.detectedLangSubject.subscribe(l => {
this.detectedLanguage(l);
});
}
private detectedLanguage(lang: DetectedLang[]) {
if (!lang) return;
if (lang.length >= 1) {
const languages = this.getConfiguredLanguages();
let firstLang = lang[0].lang;
let firstLocalLang = languages.find(x => x.iso639 == firstLang);
if (firstLocalLang) {
this.setSelectedLanguage(firstLocalLang);
return;
}
if (lang.length > 1) {
firstLang = lang[1].lang;
firstLocalLang = languages.find(x => x.iso639 == firstLang);
if (firstLocalLang) {
this.setSelectedLanguage(firstLocalLang);
return;
}
}
}
}
autoDetectLang(text: string): void {
if (!text || text.length < 5) return;
if (!this.settingsService.getSettings().disableLangAutodetec) {
this.electronService.detectLang(text);
}
}
getSelectedLanguage(): ILanguage {

View File

@ -22,14 +22,14 @@ export class MastodonWrapperService {
private readonly mastodonService: MastodonService) { }
refreshAccountIfNeeded(accountInfo: AccountInfo): Promise<AccountInfo> {
if(this.refreshingToken[accountInfo.id]){
if (this.refreshingToken[accountInfo.id]) {
return this.refreshingToken[accountInfo.id];
}
let isExpired = false;
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
if(!storedAccountInfo || !(storedAccountInfo.token))
if (!storedAccountInfo || !(storedAccountInfo.token))
return Promise.resolve(accountInfo);
try {
@ -96,7 +96,7 @@ export class MastodonWrapperService {
return this.mastodonService.getInstance(instance);
}
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation>{
translate(account: AccountInfo, statusId: string, lang: string): Promise<Translation> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.translate(refreshedAccount, statusId, lang);
@ -146,7 +146,7 @@ export class MastodonWrapperService {
}
search(account: AccountInfo, query: string, version: 'v1' | 'v2', resolve: boolean = false): Promise<Results> {
if(query.includes('twitter.com')){
if (query.includes('twitter.com')) {
query = this.processTwitterQuery(query);
}
@ -158,17 +158,17 @@ export class MastodonWrapperService {
private processTwitterQuery(query: string): string {
const settings = this.settingsService.getSettings();
if(!settings.twitterBridgeInstance) return query;
if (!settings.twitterBridgeInstance) return query;
let name;
if(query.includes('twitter.com/')){
if (query.includes('twitter.com/')) {
console.log(query.replace('https://', '').replace('http://', '').split('/'));
name = query.replace('https://', '').replace('http://', '').split('/')[1];
}
if(query.includes('@twitter.com')){
if (query.includes('@twitter.com')) {
console.log(query.split('@'));
name = query.split('@')[0];
if(name === '' || name == null){
if (name === '' || name == null) {
name = query.split('@')[1];
}
}
@ -208,7 +208,7 @@ export class MastodonWrapperService {
}
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false, resolve = true): Promise<Account[]> {
if(query.includes('twitter.com')){
if (query.includes('twitter.com')) {
query = this.processTwitterQuery(query);
}
@ -281,6 +281,20 @@ export class MastodonWrapperService {
});
}
hideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.hideBoosts(refreshedAccount, account);
});
}
unhideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unhideBoosts(refreshedAccount, account);
});
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
return this.refreshAccountIfNeeded(currentlyUsedAccount)
.then((refreshedAccount: AccountInfo) => {
@ -407,6 +421,20 @@ export class MastodonWrapperService {
});
}
blockDomain(account: AccountInfo, domain: string): Promise<void> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.blockDomain(refreshedAccount, domain);
});
}
unblockDomain(account: AccountInfo, domain: string): Promise<void> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.unblockDomain(refreshedAccount, domain);
});
}
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
@ -470,14 +498,14 @@ export class MastodonWrapperService {
});
}
getFollowing(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
getFollowing(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getFollowing(refreshedAccount, accountId, maxId, sinceId, limit);
});
}
getFollowers(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
getFollowers(account: AccountInfo, accountId: number, maxId: string, sinceId: string, limit: number = 40): Promise<FollowingResult> {
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.getFollowers(refreshedAccount, accountId, maxId, sinceId, limit);

View File

@ -357,6 +357,26 @@ export class MastodonService {
}
hideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
let input = new FormData();
input.append('reblogs', 'false');
return this.httpClient.post<Relationship>(route, input, { headers: headers }).toPromise();
}
unhideBoosts(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
let input = new FormData();
input.append('reblogs', 'true');
return this.httpClient.post<Relationship>(route, input, { headers: headers }).toPromise();
}
followHashtag(currentlyUsedAccount: AccountInfo, hashtag: string): Promise<Tag> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.followHashtag}`.replace('{0}', hashtag);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
@ -378,11 +398,13 @@ export class MastodonService {
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
let input = new FormData();
input.append('file', file);
if (description !== null && description !== undefined) {
input.append('description', description);
} else {
input.append('description', '');
}
const route = `https://${account.instance}${this.apiRoutes.uploadMediaAttachment}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Attachment>(route, input, { headers: headers }).toPromise();
@ -391,7 +413,13 @@ export class MastodonService {
//TODO: add focus support
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
let input = new FormData();
input.append('description', description);
if (description !== null && description !== undefined) {
input.append('description', description);
} else {
input.append('description', '');
}
const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
@ -524,6 +552,23 @@ export class MastodonService {
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
blockDomain(account: AccountInfo, domain: string): Promise<void> {
let route = `https://${account.instance}${this.apiRoutes.blockDomain}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
let input = new FormData();
input.append('domain', domain);
return this.httpClient.post<void>(route, input, { headers: headers }).toPromise();
}
unblockDomain(account: AccountInfo, domain: string): Promise<void> {
let route = `https://${account.instance}${this.apiRoutes.blockDomain}?domain=${domain}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}`});
return this.httpClient.delete<void>(route, { headers: headers }).toPromise();
}
pinOnProfile(account: AccountInfo, statusId: string): Promise<Status> {
let route = `https://${account.instance}${this.apiRoutes.pinStatus}`.replace('{0}', statusId.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });

View File

@ -12,6 +12,7 @@ export class ApiRoutes {
unfollow = '/api/v1/accounts/{0}/unfollow';
block = '/api/v1/accounts/{0}/block';
unblock = '/api/v1/accounts/{0}/unblock';
blockDomain = '/api/v1/domain_blocks';
mute = '/api/v1/accounts/{0}/mute';
unmute = '/api/v1/accounts/{0}/unmute';
muteStatus = '/api/v1/statuses/{0}/mute';

View File

@ -124,6 +124,7 @@ export interface Instancev1 extends Instance {
urls: InstanceUrls;
contact_account: Account;
max_toot_chars: number;
configuration: Instancev2Configuration;
}
export interface Instancev2 extends Instance {
@ -193,6 +194,33 @@ export interface Results {
hashtags: string[];
}
export interface FilterKeyword {
id: string;
keyword: string;
whole_word: boolean;
}
export interface FilterStatus {
id: string;
status_id: string;
}
export interface Filter {
id: string;
title: string;
context: string[]; //home notifications public thread account
expires_at: string;
filter_action: string; //warn hide
keywords: FilterKeyword[];
statuses: FilterStatus[];
}
export interface FilterResult {
filter: Filter;
keyword_matches: string[];
status_matches: string[];
}
export interface Status {
id: string;
uri: string;
@ -223,6 +251,7 @@ export interface Status {
bookmarked: boolean;
card: Card;
poll: Poll;
filtered: FilterResult[];
pleroma: PleromaStatusInfo;
}

View File

@ -10,6 +10,7 @@ export class NavigationService {
activatedPanelSubject = new BehaviorSubject<OpenLeftPanelEvent>(new OpenLeftPanelEvent(LeftPanelType.Closed));
activatedMediaSubject: Subject<OpenMediaEvent> = new Subject<OpenMediaEvent>();
columnSelectedSubject = new BehaviorSubject<number>(-1);
enableDraggableIconMenu = new BehaviorSubject<boolean>(false);
constructor() { }
@ -19,6 +20,10 @@ export class NavigationService {
this.activatedPanelSubject.next(newEvent);
}
changeIconMenuState(draggable: boolean) {
this.enableDraggableIconMenu.next(draggable);
}
openPanel(type: LeftPanelType){
const newEvent = new OpenLeftPanelEvent(type);
this.activatedPanelSubject.next(newEvent);

View File

@ -60,6 +60,8 @@ export class UserNotificationService {
private startFetchingNotifications(account: AccountInfo) {
let getMentionsPromise = this.mastodonService.getNotifications(account, ['favourite', 'follow', 'reblog', 'poll', 'follow_request', 'move', 'update'], null, null, 10)
.then((notifications: Notification[]) => {
notifications = notifications.filter(x => x.status !== null);
this.processMentionsAndNotifications(account, notifications, NotificationTypeEnum.UserMention);
})
.catch(err => {

View File

@ -6,6 +6,11 @@ export class AddAccount {
constructor(public account: AccountInfo) {}
}
export class ReorderAccounts {
static readonly type = '[Accounts] Reorder';
constructor(public accounts: AccountInfo[]) {}
}
export class SelectAccount {
static readonly type = '[Accounts] Select account';
constructor(public account: AccountInfo, public multiselection: boolean = false) {}
@ -46,6 +51,16 @@ export class AccountsState {
});
}
@Action(ReorderAccounts)
ReorderAccounts(ctx: StateContext<AccountsStateModel>, action: ReorderAccounts){
// const state = ctx.getState();
const reorderedAccounts = action.accounts;
ctx.patchState({
accounts: [...reorderedAccounts]
});
}
@Action(UpdateAccount)
UpdateAccount(ctx: StateContext<AccountsStateModel>, action: UpdateAccount){
const state = ctx.getState();

View File

@ -84,6 +84,10 @@ export class GlobalSettings {
configuredLanguages: ILanguage[] = [];
selectedLanguage: ILanguage;
disableLangAutodetec: boolean;
enableAltLabel: boolean;
enableFreezeAvatar: boolean;
}
export interface ILanguage {
@ -181,6 +185,9 @@ export class SettingsState {
newSettings.twitterBridgeInstance = oldSettings.twitterBridgeInstance;
newSettings.configuredLanguages = oldSettings.configuredLanguages;
newSettings.selectedLanguage = oldSettings.selectedLanguage;
newSettings.disableLangAutodetec = oldSettings.disableLangAutodetec;
newSettings.enableAltLabel = oldSettings.enableAltLabel;
newSettings.enableFreezeAvatar = oldSettings.enableFreezeAvatar;
return newSettings;
}