Merge pull request #219 from NicolasConstant/develop

0.20.0 PR
This commit is contained in:
Nicolas Constant 2020-02-20 19:35:59 -05:00 committed by GitHub
commit 993202bfff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 474 additions and 116 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "sengi", "name": "sengi",
"version": "0.19.4", "version": "0.20.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"main": "main-electron.js", "main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma", "description": "A multi-account desktop client for Mastodon and Pleroma",

View File

@ -7,7 +7,7 @@
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png"> <img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
</a> </a>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content" <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()" rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto"> (keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()" dir="auto">
</textarea> </textarea>

View File

@ -206,6 +206,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.accountSub.unsubscribe(); 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 { changePrivacy(value: string): boolean {
this.selectedPrivacy = value; this.selectedPrivacy = value;
return false; return false;
@ -225,7 +237,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private detectAutosuggestion(status: string) { private detectAutosuggestion(status: string) {
if (!this.statusLoaded) return; if (!this.statusLoaded) return;
if(!status.includes('@') && !status.includes('#')){ if (!status.includes('@') && !status.includes('#')) {
this.autosuggestData = null; this.autosuggestData = null;
this.hasSuggestions = false; this.hasSuggestions = false;
return; return;

View File

@ -1,19 +1,33 @@
<div class="panel" [class.comrade__background]="isComrade"> <div class="panel" [class.comrade__background]="isComrade">
<h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3> <h3 class="panel__title" [class.comrade__text]="isComrade">Add new account</h3>
<div class="panel__content">
<h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2> <h2 class="comrade__title" *ngIf="isComrade">Welcome Comrade!</h2>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span> account:</label> <label [class.comrade__text]="isComrade">Please provide your <span *ngIf="isComrade">comrade</span>
<input type="text" class="form-control form-control-sm form-color" [(ngModel)]="mastodonFullHandle" name="mastodonFullHandle" [class.comrade__input]="isComrade" instance:</label>
placeholder="@nickname@mastodon.social" />
<br /> <div>
<button *ngIf="!isLoading" type="submit" class="btn btn-success btn-sm" [class.comrade__button]="isComrade">Submit</button> <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> <app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
</button>
</div>
</form> </form>
<div *ngIf="isComrade" class="comrade__video"> <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> <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>
</div> </div>

View File

@ -1,21 +1,74 @@
@import "variables"; @import "variables";
@import "mixins";
@import "panel"; @import "panel";
.panel { $button-size: 70px;
.panel {
padding-left: 0px;
// padding-right: 0px;
background-position: 0 100%; background-position: 0 100%;
&__content {
padding-left: 5px;
}
} }
.form-color { .form-with-button {
background-color: $column-color; width: calc(100% - #{$button-size});
border-color: $button-border-color; float: left;
color: #fff;
font-size: $default-font-size;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }
font-size: $default-font-size;
height: 29px; 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;
// 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;
} }
$comrade_yellow: #ffcc00; $comrade_yellow: #ffcc00;

View File

@ -16,20 +16,17 @@ export class AddNewAccountComponent implements OnInit {
private blockList = ['gab.com', 'gab.ai', 'cyzed.com']; private blockList = ['gab.com', 'gab.ai', 'cyzed.com'];
private comradeList = ['juche.town']; private comradeList = ['juche.town'];
private username: string;
private instance: string;
isComrade: boolean; isComrade: boolean;
isLoading: boolean; isLoading: boolean;
private _mastodonFullHandle: string; private instance: string;
@Input() @Input()
set mastodonFullHandle(value: string) { set setInstance(value: string) {
this._mastodonFullHandle = value; this.instance = value;
this.checkComrad(); this.checkComrad();
} }
get mastodonFullHandle(): string { get setInstance(): string {
return this._mastodonFullHandle; return this.instance;
} }
constructor( constructor(
@ -41,11 +38,7 @@ export class AddNewAccountComponent implements OnInit {
} }
checkComrad(): any { checkComrad(): any {
let fullHandle = this.mastodonFullHandle.split('@').filter(x => x != null && x !== ''); if (this.instance) {
this.username = fullHandle[0];
this.instance = fullHandle[1];
if (this.username && this.instance) {
let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase(); let cleanInstance = this.instance.replace('http://', '').replace('https://', '').toLowerCase();
for (let b of this.comradeList) { for (let b of this.comradeList) {
if (cleanInstance == b || cleanInstance.includes(`.${b}`)) { if (cleanInstance == b || cleanInstance.includes(`.${b}`)) {
@ -59,12 +52,15 @@ export class AddNewAccountComponent implements OnInit {
} }
onSubmit(): boolean { 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) this.checkAndCreateApplication(this.instance)
.then((appData: AppData) => { .then((appData: AppData) => {
this.redirectToInstanceAuthPage(this.username, this.instance, appData); this.redirectToInstanceAuthPage(this.instance, appData);
}) })
.then(x => { .then(x => {
setTimeout(() => { setTimeout(() => {
@ -137,8 +133,8 @@ export class AddNewAccountComponent implements OnInit {
return snapshot.apps; return snapshot.apps;
} }
private redirectToInstanceAuthPage(username: string, instance: string, app: AppData) { private redirectToInstanceAuthPage(instance: string, app: AppData) {
const appDataTemp = new CurrentAuthProcess(username, instance); const appDataTemp = new CurrentAuthProcess(instance);
localStorage.setItem('tempAuth', JSON.stringify(appDataTemp)); localStorage.setItem('tempAuth', JSON.stringify(appDataTemp));
let instanceUrl = this.authService.getInstanceLoginUrl(instance, app.client_id, app.redirect_uri); let instanceUrl = this.authService.getInstanceLoginUrl(instance, app.client_id, app.redirect_uri);

View File

@ -21,6 +21,8 @@ export class FavoritesComponent implements OnInit {
isThread = false; isThread = false;
hasContentWarnings = false; hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();

View File

@ -22,6 +22,8 @@ export class MentionsComponent implements OnInit, OnDestroy {
isThread = false; isThread = false;
hasContentWarnings = false; hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>(); @Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>(); @Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();

View File

@ -3,6 +3,7 @@
@import "panel"; @import "panel";
@import "commons"; @import "commons";
@import "buttons"; @import "buttons";
.panel { .panel {
padding-left: 0px; padding-left: 0px;
padding-right: 0px; padding-right: 0px;
@ -11,66 +12,79 @@
.form-section { .form-section {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
padding-left: 5px;
} }
.form-with-button { .form-with-button {
width: calc(100% - #{$button-size}); width: calc(100% - #{$button-size});
float: left; float: left;
background-color: $column-color; // background-color: $column-color;
border-color: $button-border-color; // border-color: $button-border-color;
color: #fff; // color: #fff;
font-size: $default-font-size; // color: rgb(255, 0, 0);
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }
font-size: $default-font-size;
height: 29px; 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 { .form-button {
width: $button-size; @include clearButton;
height: 29px;
border: none;
outline: none;
cursor: pointer;
background-color: $button-background-color;
color: $button-color;
color: whitesmoke;
transition: all .2s; transition: all .2s;
background-color: $status-editor-footer-background;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
&:hover { &:hover {
background-color: $button-background-color-hover; background-color: lighten($status-editor-footer-background, 20%);
color: $button-color-hover; background-color: darken($status-editor-footer-background, 20%);
} }
outline: inherit;
&:focus {
background-color: darken($status-editor-footer-background, 20%);
}
border: 1px solid $button-border-color; width: $button-size;
border-width: 1px 1px 1px 0; 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-form-height: 70px;
.search-result-form { .search-result-form {
height: $search-form-height; height: $search-form-height;
padding-left: 10px; //padding-left: 10px;
padding-right: 10px; padding-right: 10px;
border-bottom: 1px solid #222736; //border-bottom: 1px solid #222736;
} }
.search-result-display { .search-result-display {
@ -83,11 +97,13 @@ $search-form-height: 70px;
margin-top: 10px; // &:first-of-type{ margin-top: 10px; // &:first-of-type{
padding-left: 10px; // margin-top: 10px; padding-left: 10px; // margin-top: 10px;
padding-right: 10px; // margin-top: 10px; padding-right: 10px; // margin-top: 10px;
// } // }
&__title { &__title {
text-transform: uppercase; text-transform: uppercase;
font-size: 13px; font-size: 13px;
} }
&__hashtag { &__hashtag {
border-radius: 2px; border-radius: 2px;
display: block; display: block;
@ -95,17 +111,22 @@ $search-form-height: 70px;
color: white; color: white;
text-decoration: none; text-decoration: none;
transition: all .3s; transition: all .3s;
&:hover { &:hover {
background-color: $button-background-color-hover; background-color: $button-background-color-hover;
} }
border-top: 1px solid $separator-color; border-top: 1px solid $separator-color;
&:last-of-type { &:last-of-type {
border-bottom: 1px solid $separator-color; border-bottom: 1px solid $separator-color;
} }
} }
&__status { &__status {
font-size: 15px; font-size: 15px;
border-top: 1px solid $separator-color; border-top: 1px solid $separator-color;
&:last-of-type { &:last-of-type {
border-bottom: 1px solid $separator-color; border-bottom: 1px solid $separator-color;
} }
@ -120,18 +141,22 @@ $search-form-height: 70px;
// text-decoration: underline; // text-decoration: underline;
// } // }
border-top: 1px solid $separator-color; border-top: 1px solid $separator-color;
&:last-of-type { &:last-of-type {
border-bottom: 1px solid $separator-color; border-bottom: 1px solid $separator-color;
} }
&__avatar { &__avatar {
width: 40px; width: 40px;
margin: 5px 10px 5px 5px; margin: 5px 10px 5px 5px;
float: left; float: left;
border-radius: 2px; border-radius: 2px;
} }
&__name { &__name {
margin: 7px 0 0 0; margin: 7px 0 0 0;
} }
&__fullhandle { &__fullhandle {
margin: 0 0 5px 0; margin: 0 0 5px 0;
color: $status-secondary-color; color: $status-secondary-color;
@ -139,11 +164,13 @@ $search-form-height: 70px;
// color: white; // color: white;
// } // }
} }
&:hover, &:hover,
&:hover &__fullhandle { &:hover &__fullhandle {
color: white; color: white;
text-decoration: none; text-decoration: none;
background-color: $button-background-color-hover; background-color: $button-background-color-hover;
} }
@include clearfix; @include clearfix;
} }

View File

@ -62,6 +62,8 @@ export class SearchComponent implements OnInit {
private lastAccountUsed: AccountInfo; private lastAccountUsed: AccountInfo;
private search(data: string) { private search(data: string) {
if(!data) return;
this.accounts.length = 0; this.accounts.length = 0;
this.statuses.length = 0; this.statuses.length = 0;
this.hashtags.length = 0; this.hashtags.length = 0;

View File

@ -27,7 +27,7 @@
<label class="noselect sub-section__label" for="disableSounds">disable sounds</label> <label class="noselect sub-section__label" for="disableSounds">disable sounds</label>
<br> <br>
<span class="sound__title">notification sound:</span><br /> <span class="sub-section__title">notification sound:</span><br />
<form [formGroup]="notificationForm"> <form [formGroup]="notificationForm">
<select formControlName="countryControl" (change)="onChange($event.target.value)" class="sound__select"> <select formControlName="countryControl" (change)="onChange($event.target.value)" class="sound__select">
<option [value]="s.id" *ngFor="let s of notificationSounds"> {{s.name}}</option> <option [value]="s.id" *ngFor="let s of notificationSounds"> {{s.name}}</option>
@ -35,6 +35,24 @@
</form> </form>
<a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a> <a href class="form-button sound__play" type="submit" (click)="playNotificationSound()">play</a>
</div> </div>
<h4 class="panel__subtitle">Shortcuts</h4>
<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> <h4 class="panel__subtitle">About</h4>
<p class="version">Sengi version: {{version}}</p> <p class="version">Sengi version: {{version}}</p>
@ -48,10 +66,12 @@
Clear all local data Clear all local data
</a> </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 Confirm Clear All
</a> </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 Cancel
</a> </a>
</div> </div>

View File

@ -23,9 +23,9 @@
padding: 0 5px 15px 5px; padding: 0 5px 15px 5px;
position: relative; position: relative;
&__checkbox{ &__checkbox {
position: relative; position: relative;
top:3px; top: 3px;
left: 5px; left: 5px;
margin-right: 7px; margin-right: 7px;
} }
@ -39,14 +39,20 @@
&__input { &__input {
margin-left: 5px; margin-left: 5px;
} }
}
.sound {
&__title { &__title {
display: inline-block; display: inline-block;
margin: 0 0 5px 5px; margin: 0 0 5px 5px;
}
& a {
color: white;
font-weight: bold;
text-decoration: underline;
}
}
}
.sound {
&__select { &__select {
float: left; float: left;
width: calc(100% - 75px); width: calc(100% - 75px);
@ -75,4 +81,3 @@
} }
} }
} }

View File

@ -11,6 +11,7 @@ import { UserNotificationService, NotificationSoundDefinition } from '../../../s
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'] styleUrls: ['./settings.component.scss']
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit {
notificationSounds: NotificationSoundDefinition[]; notificationSounds: NotificationSoundDefinition[];
@ -22,6 +23,8 @@ export class SettingsComponent implements OnInit {
disableSoundsEnabled: boolean; disableSoundsEnabled: boolean;
version: string; version: string;
columnShortcutEnabled: ColumnShortcut = ColumnShortcut.Ctrl;
columnShortcutChanged = false;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
@ -42,6 +45,26 @@ export class SettingsComponent implements OnInit {
this.disableAutofocusEnabled = settings.disableAutofocus; this.disableAutofocusEnabled = settings.disableAutofocus;
this.disableAvatarNotificationsEnabled = settings.disableAvatarNotifications; this.disableAvatarNotificationsEnabled = settings.disableAvatarNotifications;
this.disableSoundsEnabled = settings.disableSounds; 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) { onChange(soundId: string) {
@ -96,5 +119,10 @@ export class SettingsComponent implements OnInit {
this.isCleanningAll = false; this.isCleanningAll = false;
return false; return false;
} }
}
enum ColumnShortcut {
Ctrl = 1,
Win = 2
} }

View File

@ -11,6 +11,7 @@ import { Status, Account, Results } from '../../../../services/models/mastodon.i
import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service'; import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service';
import { NotificationService } from '../../../../services/notification.service'; import { NotificationService } from '../../../../services/notification.service';
import { StatusWrapper } from '../../../../models/common.model'; import { StatusWrapper } from '../../../../models/common.model';
import { StatusesStateService, StatusState } from '../../../../services/statuses-state.service';
@Component({ @Component({
selector: 'app-action-bar', selector: 'app-action-bar',
@ -55,10 +56,12 @@ export class ActionBarComponent implements OnInit, OnDestroy {
private accounts$: Observable<AccountInfo[]>; private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription; private accountSub: Subscription;
private statusStateSub: Subscription;
constructor( constructor(
private readonly store: Store, private readonly store: Store,
private readonly toolsService: ToolsService, private readonly toolsService: ToolsService,
private readonly statusStateService: StatusesStateService,
private readonly mastodonService: MastodonWrapperService, private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService) { private readonly notificationService: NotificationService) {
@ -79,6 +82,8 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.displayedStatus = status; this.displayedStatus = status;
} }
this.analyseMemoryStatus();
if (this.displayedStatus.visibility === 'direct') { if (this.displayedStatus.visibility === 'direct') {
this.isDM = true; this.isDM = true;
} }
@ -86,10 +91,31 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => { this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts); 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 { ngOnDestroy(): void {
this.accountSub.unsubscribe(); 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 { 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 this.bootedStatePerAccountId[account.id] = boostedStatus.reblog !== null; //FIXME: when Pleroma will return the good status
} else { } else {
let reblogged = boostedStatus.reblogged; //FIXME: when pixelfed will return the good status let reblogged = boostedStatus.reblogged; //FIXME: when pixelfed will return the good status
if(reblogged === null){ if (reblogged === null) {
reblogged = !this.bootedStatePerAccountId[account.id]; reblogged = !this.bootedStatePerAccountId[account.id];
} }
this.bootedStatePerAccountId[account.id] = reblogged; this.bootedStatePerAccountId[account.id] = reblogged;
@ -168,6 +194,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.notificationService.notifyHttpError(err, account); this.notificationService.notifyHttpError(err, account);
}) })
.then(() => { .then(() => {
this.statusStateService.statusReblogStatusChanged(this.displayedStatus.url, account.id, this.bootedStatePerAccountId[account.id]);
this.boostIsLoading = false; this.boostIsLoading = false;
}); });
@ -192,7 +219,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
}) })
.then((favoritedStatus: Status) => { .then((favoritedStatus: Status) => {
let favourited = favoritedStatus.favourited; //FIXME: when pixelfed will return the good status let favourited = favoritedStatus.favourited; //FIXME: when pixelfed will return the good status
if(favourited === null){ if (favourited === null) {
favourited = !this.favoriteStatePerAccountId[account.id]; favourited = !this.favoriteStatePerAccountId[account.id];
} }
this.favoriteStatePerAccountId[account.id] = favourited; this.favoriteStatePerAccountId[account.id] = favourited;
@ -202,6 +229,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
this.notificationService.notifyHttpError(err, account); this.notificationService.notifyHttpError(err, account);
}) })
.then(() => { .then(() => {
this.statusStateService.statusFavoriteStatusChanged(this.displayedStatus.url, account.id, this.favoriteStatePerAccountId[account.id]);
this.favoriteIsLoading = false; this.favoriteIsLoading = false;
}); });
@ -227,7 +255,7 @@ export class ActionBarComponent implements OnInit, OnDestroy {
} }
} }
browseThread(event: OpenThreadEvent){ browseThread(event: OpenThreadEvent) {
this.browseThreadEvent.next(event); this.browseThreadEvent.next(event);
} }
} }

View File

@ -56,6 +56,7 @@ export class StatusComponent implements OnInit {
this._statusWrapper = value; this._statusWrapper = value;
// console.warn(value.status); // console.warn(value.status);
this.status = value.status; this.status = value.status;
this.isSelected = value.isSelected;
if (this.status.reblog) { if (this.status.reblog) {
this.reblog = true; this.reblog = true;

View File

@ -5,6 +5,9 @@
</a> </a>
</div> </div>
<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 class="stream-toots__content flexcroll" #statusstream (scroll)="onScroll()" tabindex="0">
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div> <div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>

View File

@ -5,7 +5,7 @@
height: calc(100%); height: calc(100%);
width: calc(100%); width: calc(100%);
// overflow: auto; overflow: auto;
position: relative; position: relative;
&__error { &__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) { &__status:not(:last-child) {
border: solid #06070b; border: solid #06070b;
border-width: 0 0 1px 0; border-width: 0 0 1px 0;

View File

@ -28,7 +28,7 @@ export class StreamStatusesComponent implements OnInit, OnDestroy {
private websocketStreaming: StreamingWrapper; private websocketStreaming: StreamingWrapper;
statuses: StatusWrapper[] = []; statuses: StatusWrapper[] = [];
private bufferStream: Status[] = []; bufferStream: Status[] = [];
private bufferWasCleared: boolean; private bufferWasCleared: boolean;
private hideBoosts: boolean; private hideBoosts: boolean;

View File

@ -23,6 +23,8 @@ export class ThreadComponent implements OnInit, OnDestroy {
isThread = true; isThread = true;
hasContentWarnings = false; hasContentWarnings = false;
bufferStream: Status[] = []; //html compatibility only
private lastThreadEvent: OpenThreadEvent; private lastThreadEvent: OpenThreadEvent;
@Output() browseAccountEvent = new EventEmitter<string>(); @Output() browseAccountEvent = new EventEmitter<string>();
@ -163,8 +165,12 @@ export class ThreadComponent implements OnInit, OnDestroy {
let contextStatuses = [...context.ancestors, status, ...context.descendants] let contextStatuses = [...context.ancestors, status, ...context.descendants]
const position = context.ancestors.length; const position = context.ancestors.length;
for (const s of contextStatuses) { for (let i = 0; i < contextStatuses.length; i++) {
let s = contextStatuses[i];
const wrapper = new StatusWrapper(s, currentAccount); const wrapper = new StatusWrapper(s, currentAccount);
if(i == position) wrapper.isSelected = true;
this.statuses.push(wrapper); this.statuses.push(wrapper);
} }
@ -177,7 +183,6 @@ export class ThreadComponent implements OnInit, OnDestroy {
.then((position: number) => { .then((position: number) => {
setTimeout(() => { setTimeout(() => {
const el = this.statusChildren.toArray()[position]; 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' });

View File

@ -5,6 +5,7 @@ import { HotkeysService, Hotkey } from 'angular2-hotkeys';
import { StreamElement, StreamTypeEnum } from '../../states/streams.state'; import { StreamElement, StreamTypeEnum } from '../../states/streams.state';
import { NavigationService } from '../../services/navigation.service'; import { NavigationService } from '../../services/navigation.service';
import { ToolsService } from '../../services/tools.service';
@Component({ @Component({
selector: 'app-streams-selection-footer', selector: 'app-streams-selection-footer',
@ -16,11 +17,15 @@ export class StreamsSelectionFooterComponent implements OnInit {
private streams$: Observable<StreamElement[]>; private streams$: Observable<StreamElement[]>;
constructor( constructor(
private readonly toolsService: ToolsService,
private readonly hotkeysService: HotkeysService, private readonly hotkeysService: HotkeysService,
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService,
private readonly store: Store) { private readonly store: Store) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams); this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
const settings = this.toolsService.getSettings();
if(!settings.columnSwitchingWinAlt) {
this.hotkeysService.add(new Hotkey('ctrl+right', (event: KeyboardEvent): boolean => { this.hotkeysService.add(new Hotkey('ctrl+right', (event: KeyboardEvent): boolean => {
this.nextColumnSelected(); this.nextColumnSelected();
return false; return false;
@ -30,6 +35,17 @@ export class StreamsSelectionFooterComponent implements OnInit {
this.previousColumnSelected(); this.previousColumnSelected();
return false; 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() { ngOnInit() {

View File

@ -17,4 +17,6 @@ export class StatusWrapper {
public status: Status, public status: Status,
public provider: AccountInfo public provider: AccountInfo
) { } ) { }
public isSelected: boolean;
} }

View File

@ -59,5 +59,5 @@ export class AuthService {
} }
export class CurrentAuthProcess { export class CurrentAuthProcess {
constructor(public username: string, public instance: string) { } constructor(public instance: string) { }
} }

View 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();
});
});

View 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) {
}
}

View File

@ -35,6 +35,9 @@ export class GlobalSettings {
disableSounds = false; disableSounds = false;
notificationSoundFileId: string = '0'; notificationSoundFileId: string = '0';
columnSwitchingWinAlt = false;
accountSettings: AccountSettings[] = []; accountSettings: AccountSettings[] = [];
} }
@ -101,6 +104,7 @@ export class SettingsState {
newSettings.disableAvatarNotifications = oldSettings.disableAvatarNotifications; newSettings.disableAvatarNotifications = oldSettings.disableAvatarNotifications;
newSettings.disableSounds = oldSettings.disableSounds; newSettings.disableSounds = oldSettings.disableSounds;
newSettings.notificationSoundFileId = oldSettings.notificationSoundFileId; newSettings.notificationSoundFileId = oldSettings.notificationSoundFileId;
newSettings.columnSwitchingWinAlt = oldSettings.columnSwitchingWinAlt;
return newSettings; return newSettings;
} }