Merge branch 'topic_auto-update' of https://github.com/NicolasConstant/sengis into topic_auto-update

This commit is contained in:
Nicolas Constant 2020-02-25 23:08:04 -05:00
commit 611bccc383
No known key found for this signature in database
GPG Key ID: 1E9F677FB01A5688
31 changed files with 1573 additions and 658 deletions

View File

@ -37,7 +37,7 @@
<h4 class="header__download-box--subtitle">Try it in your browser!</h4>
<a href="#" class="download-button download-button__web"
title="what are you waiting for? click!"
onClick="window.open('http://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
onClick="window.open('https://sengi.nicolas-constant.com'+'?qt='+ (new Date()).getTime(),'Sengi','toolbar=no,location=no,status=no,menubar=no,scrollbars=no, resizable=yes,width=377,height=800'); return false;"
class="button"><i class="fas fa-globe"></i><span
class="download-button__web--label">launch!</span></a><br />
<br />

1603
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,11 +9,10 @@
"author": "Nicolas Constant",
"license": "WTFPL",
"devDependencies": {
"node-sass": "^4.11.0"
},
"dependencies": {
"gulp": "~3.9.1",
"node-sass": "^4.13.0",
"gulp": "^3.9.1",
"gulp-run": "^1.7.1",
"gulp-sass": "^4.0.1"
}
},
"dependencies": {}
}

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.19.3",
"version": "0.20.1",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
@ -12,7 +12,7 @@
"type": "git",
"url": "https://github.com/NicolasConstant/sengi.git"
},
"scripts": {
"scripts": {
"ng": "ng",
"start": "ng serve",
"start-mem": "node --max_old_space_size=5048 ./node_modules/@angular/cli/bin/ng serve",

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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.trim();
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);

View File

@ -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>();

View File

@ -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>();

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -92,7 +92,7 @@
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content"
(click)="removeContentWarning()">
<span class="status__content-warning--title">sensitive content</span>
{{ contentWarningText }}
<span innerHTML="{{ contentWarningText }}"></span>
</a>
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent"
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"

View File

@ -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;
@ -96,7 +97,7 @@ export class StatusComponent implements OnInit {
private checkContentWarning(status: Status) {
if (status.sensitive || status.spoiler_text) {
this.isContentWarned = true;
this.contentWarningText = status.spoiler_text;
this.contentWarningText = this.emojiConverter.applyEmojis(this.displayedStatus.emojis, status.spoiler_text, EmojiTypeEnum.medium);
}
}

View File

@ -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 -->

View File

@ -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;

View File

@ -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;

View File

@ -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);
})

View File

@ -157,7 +157,7 @@ export class UserProfileComponent implements OnInit {
this.isLoading = false;
this.statusLoading = true;
this.displayedAccount = account;
this.displayedAccount = this.fixPleromaFieldsUrl(account);
this.hasNote = account && account.note && account.note !== '<p></p>';
if (this.hasNote) {
this.note = this.emojiConverter.applyEmojis(account.emojis, account.note, EmojiTypeEnum.medium);
@ -178,6 +178,18 @@ export class UserProfileComponent implements OnInit {
});
}
private fixPleromaFieldsUrl(acc: Account): Account {
if(acc.fields){
acc.fields.forEach(f => {
if(f.value.includes('<a href="') && !f.value.includes('target="_blank"')){
f.value = f.value.replace('<a href="', '<a target="_blank" href="');
}
});
}
return acc;
}
private getPinnedStatuses(userAccount: AccountInfo, account: Account): Promise<void> {
return this.mastodonService.getAccountStatuses(userAccount, account.id, false, true, false, null, null, 20)
.then((statuses: Status[]) => {

View File

@ -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() {

View File

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

View File

@ -59,5 +59,5 @@ export class AuthService {
}
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;
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;
}

View File

@ -14,6 +14,7 @@
body {
box-sizing: border-box;
text-align: start;
}
html, body {
@ -29,6 +30,10 @@ html, body {
background-color: $color-primary;
overflow: hidden;
}
p {
unicode-bidi: plaintext;
}
// .invisible {
// display: none;
@ -42,4 +47,4 @@ html, body {
// #toot-content a {
// color: #bec3d8;
// }
// }