mirror of
https://github.com/NicolasConstant/sengi
synced 2025-02-09 08:28:40 +01:00
commit
993202bfff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sengi",
|
||||
"version": "0.19.4",
|
||||
"version": "0.20.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "main-electron.js",
|
||||
"description": "A multi-account desktop client for Mastodon and Pleroma",
|
||||
|
@ -1,5 +1,5 @@
|
||||
<form class="status-editor" (ngSubmit)="onSubmit()">
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" name="title"
|
||||
autocomplete="off" placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" dir="auto" />
|
||||
|
||||
<a class="status-editor__emoji" title="Insert Emoji"
|
||||
@ -7,7 +7,7 @@
|
||||
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
|
||||
</a>
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content"
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" (paste)="onPaste($event)"
|
||||
rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
|
||||
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
|
||||
</textarea>
|
||||
|
@ -206,6 +206,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
onPaste(e: any) {
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
let blobs: File[] = [];
|
||||
for (const item of items) {
|
||||
if (item.type.indexOf('image') === 0) {
|
||||
let blob = item.getAsFile();
|
||||
blobs.push(blob);
|
||||
}
|
||||
}
|
||||
this.handleFileInput(blobs);
|
||||
}
|
||||
|
||||
changePrivacy(value: string): boolean {
|
||||
this.selectedPrivacy = value;
|
||||
return false;
|
||||
@ -224,8 +236,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
|
||||
private detectAutosuggestion(status: string) {
|
||||
if (!this.statusLoaded) return;
|
||||
|
||||
if(!status.includes('@') && !status.includes('#')){
|
||||
|
||||
if (!status.includes('@') && !status.includes('#')) {
|
||||
this.autosuggestData = null;
|
||||
this.hasSuggestions = false;
|
||||
return;
|
||||
|
@ -1,19 +1,33 @@
|
||||
<div class="panel" [class.comrade__background]="isComrade">
|
||||
<h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3>
|
||||
|
||||
<h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span> account:</label>
|
||||
<input type="text" class="form-control form-control-sm form-color" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle" [class.comrade__input]="isComrade"
|
||||
placeholder="@nickname@mastodon.social" />
|
||||
<br />
|
||||
<button *ngIf="!isLoading" type="submit" class="btn btn-success btn-sm" [class.comrade__button]="isComrade">Submit</button>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</form>
|
||||
<div class="panel__content">
|
||||
<h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2>
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span>
|
||||
instance:</label>
|
||||
|
||||
<div *ngIf="isComrade" class="comrade__video">
|
||||
<iframe width="300" height="170" src="https://www.youtube.com/embed/NzBjnoRG7Mo?feature=oembed&autoplay=1&auto_play=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<div>
|
||||
<input type="text" class="form-control form-control-sm form-with-button"
|
||||
[(ngModel)]="setInstance" name="instance" [class.comrade__input]="isComrade"
|
||||
placeholder="mastodon.social" />
|
||||
|
||||
<button type="submit" class="form-button"
|
||||
title="add account"
|
||||
[class.comrade__button]="isComrade">
|
||||
<span *ngIf="!isLoading">Submit</span>
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div *ngIf="isComrade" class="comrade__video">
|
||||
<iframe width="300" height="170"
|
||||
src="https://www.youtube.com/embed/NzBjnoRG7Mo?feature=oembed&autoplay=1&auto_play=1" frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -1,21 +1,74 @@
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
|
||||
$button-size: 70px;
|
||||
|
||||
.panel {
|
||||
|
||||
padding-left: 0px;
|
||||
// padding-right: 0px;
|
||||
background-position: 0 100%;
|
||||
|
||||
&__content {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-color {
|
||||
background-color: $column-color;
|
||||
border-color: $button-border-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
.form-with-button {
|
||||
width: calc(100% - #{$button-size});
|
||||
float: left;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
font-size: $default-font-size;
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
background-color: $status-editor-title-background;
|
||||
color: $status-editor-color;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-width: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
// background-color: $column-color;
|
||||
// border-color: $button-border-color;
|
||||
// color: #fff;
|
||||
// font-size: $default-font-size;
|
||||
// &:focus {
|
||||
// box-shadow: none;
|
||||
// }
|
||||
// height: 29px;
|
||||
// padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
.waiting-icon {
|
||||
position: relative;
|
||||
top:1px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
.form-button {
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
background-color: $status-editor-footer-background;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
$comrade_yellow: #ffcc00;
|
||||
|
@ -16,20 +16,17 @@ export class AddNewAccountComponent implements OnInit {
|
||||
private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
|
||||
private comradeList = ['juche.town'];
|
||||
|
||||
private username: string;
|
||||
private instance: string;
|
||||
isComrade: boolean;
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
private _mastodonFullHandle: string;
|
||||
private instance: string;
|
||||
@Input()
|
||||
set mastodonFullHandle(value: string) {
|
||||
this._mastodonFullHandle = value;
|
||||
set setInstance(value: string) {
|
||||
this.instance = value;
|
||||
this.checkComrad();
|
||||
}
|
||||
get mastodonFullHandle(): string {
|
||||
return this._mastodonFullHandle;
|
||||
get setInstance(): string {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ -41,11 +38,7 @@ export class AddNewAccountComponent implements OnInit {
|
||||
}
|
||||
|
||||
checkComrad(): any {
|
||||
let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== '');
|
||||
this.username = fullHandle[0];
|
||||
this.instance = fullHandle[1];
|
||||
|
||||
if (this.username && this.instance) {
|
||||
if (this.instance) {
|
||||
let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase();
|
||||
for (let b of this.comradeList) {
|
||||
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
|
||||
@ -59,12 +52,15 @@ export class AddNewAccountComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
this.checkBlockList(this.instance);
|
||||
if(this.isLoading || !this.instance) return false;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isLoading = true;
|
||||
|
||||
this.checkBlockList(this.instance);
|
||||
|
||||
this.checkAndCreateApplication(this.instance)
|
||||
.then((appData: AppData) => {
|
||||
this.redirectToInstanceAuthPage(this.username, this.instance, appData);
|
||||
this.redirectToInstanceAuthPage(this.instance, appData);
|
||||
})
|
||||
.then(x => {
|
||||
setTimeout(() => {
|
||||
@ -137,8 +133,8 @@ export class AddNewAccountComponent implements OnInit {
|
||||
return snapshot.apps;
|
||||
}
|
||||
|
||||
private redirectToInstanceAuthPage(username: string, instance: string, app: AppData) {
|
||||
const appDataTemp = new CurrentAuthProcess(username, instance);
|
||||
private redirectToInstanceAuthPage(instance: string, app: AppData) {
|
||||
const appDataTemp = new CurrentAuthProcess(instance);
|
||||
localStorage.setItem('tempAuth', JSON.stringify(appDataTemp));
|
||||
|
||||
let instanceUrl = this.authService.getInstanceLoginUrl(instance, app.client_id, app.redirect_uri);
|
||||
|
@ -21,6 +21,8 @@ export class FavoritesComponent implements OnInit {
|
||||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
bufferStream: Status[] = []; //html compatibility only
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
@ -22,6 +22,8 @@ export class MentionsComponent implements OnInit, OnDestroy {
|
||||
isThread = false;
|
||||
hasContentWarnings = false;
|
||||
|
||||
bufferStream: Status[] = []; //html compatibility only
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@Output() browseHashtagEvent = new EventEmitter<string>();
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
|
@ -3,6 +3,7 @@
|
||||
@import "panel";
|
||||
@import "commons";
|
||||
@import "buttons";
|
||||
|
||||
.panel {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
@ -11,66 +12,79 @@
|
||||
.form-section {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.form-with-button {
|
||||
width: calc(100% - #{$button-size});
|
||||
float: left;
|
||||
|
||||
background-color: $column-color;
|
||||
border-color: $button-border-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
// background-color: $column-color;
|
||||
// border-color: $button-border-color;
|
||||
// color: #fff;
|
||||
// color: rgb(255, 0, 0);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
font-size: $default-font-size;
|
||||
height: 29px;
|
||||
padding: 0 5px 0 5px;
|
||||
padding: 0 5px 0 5px;
|
||||
|
||||
background-color: $status-editor-title-background;
|
||||
color: $status-editor-color;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-width: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
// .form-control {
|
||||
// margin: 0 0 5px 5px;
|
||||
// width: calc(100% - 10px);
|
||||
// background-color: $column-color;
|
||||
// border-color: $status-secondary-color;
|
||||
// color: #fff;
|
||||
// font-size: $default-font-size;
|
||||
// &:focus {
|
||||
// box-shadow: none;
|
||||
// }
|
||||
// // &--privacy {
|
||||
// // display: inline-block;
|
||||
// // width: calc(100% - 15px - #{$btn-send-status-width} - #{$counter-width});
|
||||
// // }
|
||||
// }
|
||||
|
||||
.form-button {
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: $button-background-color;
|
||||
color: $button-color;
|
||||
color: whitesmoke;
|
||||
@include clearButton;
|
||||
transition: all .2s;
|
||||
background-color: $status-editor-footer-background;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
color: $button-color-hover;
|
||||
background-color: lighten($status-editor-footer-background, 20%);
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
|
||||
border: 1px solid $button-border-color;
|
||||
border-width: 1px 1px 1px 0;
|
||||
outline: inherit;
|
||||
&:focus {
|
||||
background-color: darken($status-editor-footer-background, 20%);
|
||||
}
|
||||
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
|
||||
// border: none;
|
||||
// outline: none;
|
||||
// cursor: pointer;
|
||||
// background-color: $button-background-color;
|
||||
// color: $button-color;
|
||||
// color: whitesmoke;
|
||||
// transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: $button-background-color-hover;
|
||||
// color: $button-color-hover;
|
||||
// }
|
||||
|
||||
|
||||
// border: 1px solid $button-border-color;
|
||||
// border-width: 1px 1px 1px 0;
|
||||
}
|
||||
|
||||
$search-form-height: 70px;
|
||||
|
||||
.search-result-form {
|
||||
height: $search-form-height;
|
||||
padding-left: 10px;
|
||||
//padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-bottom: 1px solid #222736;
|
||||
//border-bottom: 1px solid #222736;
|
||||
}
|
||||
|
||||
.search-result-display {
|
||||
@ -83,11 +97,13 @@ $search-form-height: 70px;
|
||||
margin-top: 10px; // &:first-of-type{
|
||||
padding-left: 10px; // margin-top: 10px;
|
||||
padding-right: 10px; // margin-top: 10px;
|
||||
|
||||
// }
|
||||
&__title {
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__hashtag {
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
@ -95,17 +111,22 @@ $search-form-height: 70px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 15px;
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
@ -120,18 +141,22 @@ $search-form-height: 70px;
|
||||
// text-decoration: underline;
|
||||
// }
|
||||
border-top: 1px solid $separator-color;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 40px;
|
||||
margin: 5px 10px 5px 5px;
|
||||
float: left;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin: 7px 0 0 0;
|
||||
}
|
||||
|
||||
&__fullhandle {
|
||||
margin: 0 0 5px 0;
|
||||
color: $status-secondary-color;
|
||||
@ -139,11 +164,13 @@ $search-form-height: 70px;
|
||||
// color: white;
|
||||
// }
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
@include clearfix;
|
||||
}
|
||||
}
|
@ -62,6 +62,8 @@ export class SearchComponent implements OnInit {
|
||||
|
||||
private lastAccountUsed: AccountInfo;
|
||||
private search(data: string) {
|
||||
if(!data) return;
|
||||
|
||||
this.accounts.length = 0;
|
||||
this.statuses.length = 0;
|
||||
this.hashtags.length = 0;
|
||||
|
@ -27,7 +27,7 @@
|
||||
<label class="noselect sub-section__label" for="disableSounds">disable sounds</label>
|
||||
<br>
|
||||
|
||||
<span class="sound__title">notification sound:</span><br />
|
||||
<span class="sub-section__title">notification sound:</span><br />
|
||||
<form [formGroup]="notificationForm">
|
||||
<select formControlName="countryControl" (change)="onChange($event.target.value)" class="sound__select">
|
||||
<option [value]="s.id" *ngFor="let s of notificationSounds"> {{s.name}}</option>
|
||||
@ -35,6 +35,24 @@
|
||||
</form>
|
||||
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
|
||||
</div>
|
||||
<h4 class="panel__subtitle">Shortcuts</h4>
|
||||
<div class="sub-section">
|
||||
<span class="sub-section__title">switch column:</span><br />
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="columnShortcutEnabled === 1"
|
||||
(change)="onShortcutChange(1)" type="radio" name="column-ctrl" value="column-ctrl"
|
||||
id="column-ctrl">
|
||||
<label class="noselect sub-section__label" for="column-ctrl">Ctrl + Left | Ctrl + Right</label>
|
||||
<br>
|
||||
|
||||
<input class="sub-section__checkbox" [checked]="columnShortcutEnabled === 2"
|
||||
(change)="onShortcutChange(2)" type="radio" name="colmun-win"
|
||||
value="colmun-win" id="colmun-win">
|
||||
<label class="noselect sub-section__label" for="colmun-win">Win + Alt + Left | Win + Alt + Right</label>
|
||||
<br>
|
||||
|
||||
<span class="sub-section__title" *ngIf="columnShortcutChanged">this settings needs a <a href (click)="reload()">reload</a> to be effective.</span>
|
||||
</div>
|
||||
|
||||
<h4 class="panel__subtitle">About</h4>
|
||||
<p class="version">Sengi version: {{version}}</p>
|
||||
@ -48,10 +66,12 @@
|
||||
Clear all local data
|
||||
</a>
|
||||
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__red sengi-btn__medium" href (click)="confirmClearAll()">
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__red sengi-btn__medium" href
|
||||
(click)="confirmClearAll()">
|
||||
Confirm Clear All
|
||||
</a>
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__blue sengi-btn__medium" href (click)="cancelClearAll()">
|
||||
<a *ngIf="isCleanningAll" class="sengi-btn sengi-btn__blue sengi-btn__medium" href
|
||||
(click)="cancelClearAll()">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
|
@ -23,12 +23,12 @@
|
||||
padding: 0 5px 15px 5px;
|
||||
position: relative;
|
||||
|
||||
&__checkbox{
|
||||
&__checkbox {
|
||||
position: relative;
|
||||
top:3px;
|
||||
top: 3px;
|
||||
left: 5px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: block;
|
||||
@ -39,14 +39,20 @@
|
||||
&__input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sound {
|
||||
&__title {
|
||||
display: inline-block;
|
||||
margin: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sound {
|
||||
&__select {
|
||||
float: left;
|
||||
width: calc(100% - 75px);
|
||||
@ -54,7 +60,7 @@
|
||||
margin-left: 5px;
|
||||
|
||||
background-color: #32384d;
|
||||
color: white;
|
||||
color: white;
|
||||
border: 1px solid #32384d;
|
||||
}
|
||||
|
||||
@ -74,5 +80,4 @@
|
||||
background-color: #32384d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import { UserNotificationService, NotificationSoundDefinition } from '../../../s
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss']
|
||||
})
|
||||
|
||||
export class SettingsComponent implements OnInit {
|
||||
|
||||
notificationSounds: NotificationSoundDefinition[];
|
||||
@ -22,6 +23,8 @@ export class SettingsComponent implements OnInit {
|
||||
disableSoundsEnabled: boolean;
|
||||
version: string;
|
||||
|
||||
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
|
||||
columnShortcutChanged = false;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
@ -42,6 +45,26 @@ export class SettingsComponent implements OnInit {
|
||||
this.disableAutofocusEnabled = settings.disableAutofocus;
|
||||
this.disableAvatarNotificationsEnabled = settings.disableAvatarNotifications;
|
||||
this.disableSoundsEnabled = settings.disableSounds;
|
||||
|
||||
if(!settings.columnSwitchingWinAlt){
|
||||
this.columnShortcutEnabled = ColumnShortcut.Ctrl;
|
||||
} else {
|
||||
this.columnShortcutEnabled = ColumnShortcut.Win;
|
||||
}
|
||||
}
|
||||
|
||||
onShortcutChange(id: ColumnShortcut){
|
||||
this.columnShortcutEnabled = id;
|
||||
this.columnShortcutChanged = true;
|
||||
|
||||
let settings = this.toolsService.getSettings()
|
||||
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
|
||||
this.toolsService.saveSettings(settings);
|
||||
}
|
||||
|
||||
reload(): boolean {
|
||||
window.location.reload();
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange(soundId: string) {
|
||||
@ -96,5 +119,10 @@ export class SettingsComponent implements OnInit {
|
||||
this.isCleanningAll = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum ColumnShortcut {
|
||||
Ctrl = 1,
|
||||
Win = 2
|
||||
}
|
@ -11,6 +11,7 @@ import { Status, Account, Results } from '../../../../services/models/mastodon.i
|
||||
import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
import { StatusesStateService, StatusState } from '../../../../services/statuses-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-bar',
|
||||
@ -43,7 +44,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
favoriteIsLoading: boolean;
|
||||
boostIsLoading: boolean;
|
||||
|
||||
isContentWarningActive: boolean = false;
|
||||
isContentWarningActive: boolean = false;
|
||||
|
||||
displayedStatus: Status;
|
||||
|
||||
@ -55,10 +56,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
private statusStateSub: Subscription;
|
||||
|
||||
constructor(
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly statusStateService: StatusesStateService,
|
||||
private readonly mastodonService: MastodonWrapperService,
|
||||
private readonly notificationService: NotificationService) {
|
||||
|
||||
@ -79,6 +82,8 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
this.displayedStatus = status;
|
||||
}
|
||||
|
||||
this.analyseMemoryStatus();
|
||||
|
||||
if (this.displayedStatus.visibility === 'direct') {
|
||||
this.isDM = true;
|
||||
}
|
||||
@ -86,10 +91,31 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
this.checkStatus(accounts);
|
||||
});
|
||||
|
||||
this.statusStateSub = this.statusStateService.stateNotification.subscribe((state: StatusState) => {
|
||||
if (state && state.statusId === this.displayedStatus.url) {
|
||||
this.favoriteStatePerAccountId[state.accountId] = state.isFavorited;
|
||||
this.bootedStatePerAccountId[state.accountId] = state.isRebloged;
|
||||
|
||||
this.checkIfFavorited();
|
||||
this.checkIfBoosted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.accountSub.unsubscribe();
|
||||
this.statusStateSub.unsubscribe();
|
||||
}
|
||||
|
||||
private analyseMemoryStatus() {
|
||||
let memoryStatusState = this.statusStateService.getStateForStatus(this.displayedStatus.url);
|
||||
if (!memoryStatusState) return;
|
||||
|
||||
memoryStatusState.forEach((state: StatusState) => {
|
||||
this.favoriteStatePerAccountId[state.accountId] = state.isFavorited;
|
||||
this.bootedStatePerAccountId[state.accountId] = state.isRebloged;
|
||||
});
|
||||
}
|
||||
|
||||
private checkStatus(accounts: AccountInfo[]): void {
|
||||
@ -156,7 +182,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
this.bootedStatePerAccountId[account.id] = boostedStatus.reblog !== null; //FIXME: when Pleroma will return the good status
|
||||
} else {
|
||||
let reblogged = boostedStatus.reblogged; //FIXME: when pixelfed will return the good status
|
||||
if(reblogged === null){
|
||||
if (reblogged === null) {
|
||||
reblogged = !this.bootedStatePerAccountId[account.id];
|
||||
}
|
||||
this.bootedStatePerAccountId[account.id] = reblogged;
|
||||
@ -168,6 +194,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
})
|
||||
.then(() => {
|
||||
this.statusStateService.statusReblogStatusChanged(this.displayedStatus.url, account.id, this.bootedStatePerAccountId[account.id]);
|
||||
this.boostIsLoading = false;
|
||||
});
|
||||
|
||||
@ -192,7 +219,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
.then((favoritedStatus: Status) => {
|
||||
let favourited = favoritedStatus.favourited; //FIXME: when pixelfed will return the good status
|
||||
if(favourited === null){
|
||||
if (favourited === null) {
|
||||
favourited = !this.favoriteStatePerAccountId[account.id];
|
||||
}
|
||||
this.favoriteStatePerAccountId[account.id] = favourited;
|
||||
@ -202,6 +229,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
this.notificationService.notifyHttpError(err, account);
|
||||
})
|
||||
.then(() => {
|
||||
this.statusStateService.statusFavoriteStatusChanged(this.displayedStatus.url, account.id, this.favoriteStatePerAccountId[account.id]);
|
||||
this.favoriteIsLoading = false;
|
||||
});
|
||||
|
||||
@ -225,9 +253,9 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.isFavorited = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
browseThread(event: OpenThreadEvent){
|
||||
browseThread(event: OpenThreadEvent) {
|
||||
this.browseThreadEvent.next(event);
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ export class StatusComponent implements OnInit {
|
||||
this._statusWrapper = value;
|
||||
// console.warn(value.status);
|
||||
this.status = value.status;
|
||||
this.isSelected = value.isSelected;
|
||||
|
||||
if (this.status.reblog) {
|
||||
this.reblog = true;
|
||||
|
@ -5,7 +5,10 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div class="stream-toots__new-notification"
|
||||
[class.stream-toots__new-notification--display]="bufferStream && bufferStream.length > 0"></div>
|
||||
|
||||
<div class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
|
||||
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>
|
||||
|
||||
<!-- data-simplebar -->
|
||||
|
@ -5,7 +5,7 @@
|
||||
height: calc(100%);
|
||||
width: calc(100%);
|
||||
|
||||
// overflow: auto;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
&__error {
|
||||
@ -22,6 +22,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__new-notification {
|
||||
z-index: 1;
|
||||
width: $stream-column-width;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0), rgba(255, 255, 255, 0));
|
||||
background: radial-gradient(ellipse at center, rgba(150, 192, 255, 0.514), rgba(255, 255, 255, 0), rgba(255, 255, 255, 0));
|
||||
transition: all .5s;
|
||||
opacity: 0;
|
||||
|
||||
&--display {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__status:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
|
@ -28,7 +28,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
|
||||
private websocketStreaming: StreamingWrapper;
|
||||
|
||||
statuses: StatusWrapper[] = [];
|
||||
private bufferStream: Status[] = [];
|
||||
bufferStream: Status[] = [];
|
||||
private bufferWasCleared: boolean;
|
||||
|
||||
private hideBoosts: boolean;
|
||||
|
@ -23,6 +23,8 @@ export class ThreadComponent implements OnInit, OnDestroy {
|
||||
isThread = true;
|
||||
hasContentWarnings = false;
|
||||
|
||||
bufferStream: Status[] = []; //html compatibility only
|
||||
|
||||
private lastThreadEvent: OpenThreadEvent;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@ -159,12 +161,16 @@ export class ThreadComponent implements OnInit, OnDestroy {
|
||||
pipeline
|
||||
.then((status: Status) => {
|
||||
return this.mastodonService.getStatusContext(currentAccount, status.id)
|
||||
.then((context: Context) => {
|
||||
.then((context: Context) => {
|
||||
let contextStatuses = [...context.ancestors, status, ...context.descendants]
|
||||
const position = context.ancestors.length;
|
||||
|
||||
for (const s of contextStatuses) {
|
||||
const wrapper = new StatusWrapper(s, currentAccount);
|
||||
for (let i = 0; i < contextStatuses.length; i++) {
|
||||
let s = contextStatuses[i];
|
||||
const wrapper = new StatusWrapper(s, currentAccount);
|
||||
|
||||
if(i == position) wrapper.isSelected = true;
|
||||
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
|
||||
@ -177,10 +183,9 @@ export class ThreadComponent implements OnInit, OnDestroy {
|
||||
.then((position: number) => {
|
||||
setTimeout(() => {
|
||||
const el = this.statusChildren.toArray()[position];
|
||||
el.isSelected = true;
|
||||
|
||||
//el.elem.nativeElement.scrollIntoViewIfNeeded({ behavior: 'auto', block: 'start', inline: 'nearest' });
|
||||
|
||||
//el.elem.nativeElement.scrollIntoViewIfNeeded({ behavior: 'auto', block: 'start', inline: 'nearest' });
|
||||
|
||||
scrollIntoView(el.elem.nativeElement, { behavior: 'smooth', block: 'nearest'});
|
||||
}, 250);
|
||||
})
|
||||
|
@ -5,6 +5,7 @@ import { HotkeysService, Hotkey } from 'angular2-hotkeys';
|
||||
|
||||
import { StreamElement, StreamTypeEnum } from '../../states/streams.state';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-streams-selection-footer',
|
||||
@ -16,20 +17,35 @@ export class StreamsSelectionFooterComponent implements OnInit {
|
||||
private streams$: Observable<StreamElement[]>;
|
||||
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly hotkeysService: HotkeysService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly store: Store) {
|
||||
|
||||
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
|
||||
|
||||
this.hotkeysService.add(new Hotkey('ctrl+right', (event: KeyboardEvent): boolean => {
|
||||
this.nextColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
|
||||
this.hotkeysService.add(new Hotkey('ctrl+left', (event: KeyboardEvent): boolean => {
|
||||
this.previousColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
const settings = this.toolsService.getSettings();
|
||||
if(!settings.columnSwitchingWinAlt) {
|
||||
this.hotkeysService.add(new Hotkey('ctrl+right', (event: KeyboardEvent): boolean => {
|
||||
this.nextColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
|
||||
this.hotkeysService.add(new Hotkey('ctrl+left', (event: KeyboardEvent): boolean => {
|
||||
this.previousColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
} else {
|
||||
this.hotkeysService.add(new Hotkey('meta+alt+right', (event: KeyboardEvent): boolean => {
|
||||
this.nextColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
|
||||
this.hotkeysService.add(new Hotkey('meta+alt+left', (event: KeyboardEvent): boolean => {
|
||||
this.previousColumnSelected();
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -17,4 +17,6 @@ export class StatusWrapper {
|
||||
public status: Status,
|
||||
public provider: AccountInfo
|
||||
) { }
|
||||
|
||||
public isSelected: boolean;
|
||||
}
|
@ -59,5 +59,5 @@ export class AuthService {
|
||||
}
|
||||
|
||||
export class CurrentAuthProcess {
|
||||
constructor(public username: string, public instance: string) { }
|
||||
constructor(public instance: string) { }
|
||||
}
|
59
src/app/services/statuses-state.service.spec.ts
Normal file
59
src/app/services/statuses-state.service.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StatusesStateService } from './statuses-state.service';
|
||||
import { setRootDomAdapter } from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
describe('StatusesStateService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: StatusesStateService = TestBed.get(StatusesStateService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set unset favorited status', () => {
|
||||
const statusId = 'statusId';
|
||||
const accountId = 'accountId';
|
||||
|
||||
const service: StatusesStateService = TestBed.get(StatusesStateService);
|
||||
service.statusFavoriteStatusChanged(statusId, accountId, true);
|
||||
let result = service.getStateForStatus(statusId).find(x => x.accountId === accountId);
|
||||
|
||||
expect(result.isFavorited).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set unset rebloged status', () => {
|
||||
const statusId = 'statusId';
|
||||
const accountId = 'accountId';
|
||||
|
||||
const service: StatusesStateService = TestBed.get(StatusesStateService);
|
||||
service.statusReblogStatusChanged(statusId, accountId, true);
|
||||
let result = service.getStateForStatus(statusId).find(x => x.accountId === accountId);
|
||||
|
||||
expect(result.isRebloged).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be able to reset favorited status', () => {
|
||||
const statusId = 'statusId';
|
||||
const accountId = 'accountId';
|
||||
|
||||
const service: StatusesStateService = TestBed.get(StatusesStateService);
|
||||
service.statusFavoriteStatusChanged(statusId, accountId, true);
|
||||
service.statusFavoriteStatusChanged(statusId, accountId, false);
|
||||
let result = service.getStateForStatus(statusId).find(x => x.accountId === accountId);
|
||||
|
||||
expect(result.isFavorited).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should be able to reset rebloged status', () => {
|
||||
const statusId = 'statusId';
|
||||
const accountId = 'accountId';
|
||||
|
||||
const service: StatusesStateService = TestBed.get(StatusesStateService);
|
||||
service.statusReblogStatusChanged(statusId, accountId, true);
|
||||
service.statusReblogStatusChanged(statusId, accountId, false);
|
||||
let result = service.getStateForStatus(statusId).find(x => x.accountId === accountId);
|
||||
|
||||
expect(result.isRebloged).toBeFalsy();
|
||||
});
|
||||
});
|
61
src/app/services/statuses-state.service.ts
Normal file
61
src/app/services/statuses-state.service.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StatusesStateService {
|
||||
private cachedStatusStates: { [statusId: string]: { [accountId: string]: StatusState } } = {};
|
||||
public stateNotification = new Subject<StatusState>();
|
||||
|
||||
constructor() { }
|
||||
|
||||
getStateForStatus(statusId: string): StatusState[] {
|
||||
if(!this.cachedStatusStates[statusId])
|
||||
return null;
|
||||
|
||||
let results: StatusState[] = [];
|
||||
Object.entries(this.cachedStatusStates[statusId]).forEach(
|
||||
([key, value]) => {
|
||||
results.push(value);
|
||||
}
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
statusFavoriteStatusChanged(statusId: string, accountId: string, isFavorited: boolean) {
|
||||
if (!this.cachedStatusStates[statusId])
|
||||
this.cachedStatusStates[statusId] = {};
|
||||
|
||||
if (!this.cachedStatusStates[statusId][accountId]) {
|
||||
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, isFavorited, false);
|
||||
} else {
|
||||
this.cachedStatusStates[statusId][accountId].isFavorited = isFavorited;
|
||||
}
|
||||
|
||||
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
|
||||
}
|
||||
|
||||
statusReblogStatusChanged(statusId: string, accountId: string, isRebloged: boolean) {
|
||||
if (!this.cachedStatusStates[statusId])
|
||||
this.cachedStatusStates[statusId] = {};
|
||||
|
||||
if (!this.cachedStatusStates[statusId][accountId]) {
|
||||
this.cachedStatusStates[statusId][accountId] = new StatusState(statusId, accountId, false, isRebloged);
|
||||
} else {
|
||||
this.cachedStatusStates[statusId][accountId].isRebloged = isRebloged;
|
||||
}
|
||||
|
||||
this.stateNotification.next(this.cachedStatusStates[statusId][accountId]);
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusState {
|
||||
constructor(
|
||||
public statusId: string,
|
||||
public accountId: string,
|
||||
public isFavorited: boolean,
|
||||
public isRebloged: boolean) {
|
||||
}
|
||||
}
|
@ -35,6 +35,9 @@ export class GlobalSettings {
|
||||
disableSounds = false;
|
||||
|
||||
notificationSoundFileId: string = '0';
|
||||
|
||||
columnSwitchingWinAlt = false;
|
||||
|
||||
accountSettings: AccountSettings[] = [];
|
||||
}
|
||||
|
||||
@ -101,6 +104,7 @@ export class SettingsState {
|
||||
newSettings.disableAvatarNotifications = oldSettings.disableAvatarNotifications;
|
||||
newSettings.disableSounds = oldSettings.disableSounds;
|
||||
newSettings.notificationSoundFileId = oldSettings.notificationSoundFileId;
|
||||
newSettings.columnSwitchingWinAlt = oldSettings.columnSwitchingWinAlt;
|
||||
|
||||
return newSettings;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user