1
0
mirror of https://github.com/NicolasConstant/sengi synced 2025-02-02 11:36:54 +01:00

Merge pull request #44 from NicolasConstant/develop

Merge for 0.2
This commit is contained in:
Nicolas Constant 2019-02-24 00:03:41 -05:00 committed by GitHub
commit c674d8f52c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 655 additions and 291 deletions

View File

@ -10,7 +10,7 @@
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy" [(ngModel)]="selectedPrivacy">
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
</select>
<button type="submit" class="btn btn-sm btn-custom-primary">TOOT!</button>
<button type="submit" class="btn btn-sm btn-custom-primary">POST!</button>
</form>

View File

@ -24,16 +24,16 @@ export class ManageAccountComponent implements OnInit {
ngOnInit() {
const instance = this.account.info.instance;
this.availableStreams.length = 0;
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, `federate@${instance}`));
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, `local@${instance}`));
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, `home@${instance}`));
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Federated Timeline', this.account.info.id, null, null, instance));
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null, instance));
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Home', this.account.info.id, null, null, instance));
}
addStream(stream: StreamElement): boolean {
if (stream) {
this.store.dispatch([new AddStream(stream)]).toPromise()
.then(() => {
this.notificationService.notify(`${stream.displayableFullName} added`, false);
this.notificationService.notify(`stream added`, false);
});
}
return false;

View File

@ -1,39 +1,44 @@
<div class="panel">
<h3 class="panel__title">search</h3>
<form class="form-section" (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle" name="searchHandle"
placeholder="Search" autocomplete="off" />
<button class="form-button" type="submit" title="search">GO</button>
</form>
<div class="search-result-form">
<h3 class="panel__title">search</h3>
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="accounts.length > 0" class="search-results">
<h3 class="search-results__title">Accounts</h3>
<a href *ngFor="let account of accounts" class="account" title="open account"
(click)="browseAccount(account.acct)">
<img src="{{account.avatar}}" class="account__avatar" />
<div class="account__name">{{ account.username }}</div>
<div class="account__fullhandle">@{{ account.acct }}</div>
</a>
<form class="form-section" (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm form-with-button" [(ngModel)]="searchHandle"
name="searchHandle" placeholder="Search" autocomplete="off" />
<button class="form-button" type="submit" title="search">GO</button>
</form>
</div>
<div *ngIf="hashtags.length > 0" class="search-results">
<h3 class="search-results__title">Hashtags</h3>
<a (click)="browseHashtag(hashtag)" href *ngFor="let hashtag of hashtags" class="search-results__hashtag" title="browse hashtag">
#{{ hashtag }}
</a>
</div>
<div class="search-result-display flexcroll">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div *ngIf="statuses.length > 0" class="search-results">
<h3 class="search-results__title">Statuses</h3>
<div *ngIf="accounts.length > 0" class="search-results">
<h3 class="search-results__title">Accounts</h3>
<a href *ngFor="let account of accounts" class="account" title="open account"
(click)="browseAccount(account.acct)">
<img src="{{account.avatar}}" class="account__avatar" />
<div class="account__name">{{ account.username }}</div>
<div class="account__fullhandle">@{{ account.acct }}</div>
</a>
</div>
<div class="search-results__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
<div *ngIf="hashtags.length > 0" class="search-results">
<h3 class="search-results__title">Hashtags</h3>
<a (click)="browseHashtag(hashtag)" href *ngFor="let hashtag of hashtags" class="search-results__hashtag"
title="browse hashtag">
#{{ hashtag }}
</a>
</div>
<div *ngIf="statuses.length > 0" class="search-results">
<h3 class="search-results__title">Statuses</h3>
<div class="search-results__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" (browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)">
</app-status>
</div>
</div>
</div>
</div>

View File

@ -2,7 +2,10 @@
@import "mixins";
@import "panel";
@import "commons";
.panel {
padding-left: 0px;
padding-right: 0px;
}
.form-section {
overflow: auto;
@ -29,10 +32,24 @@
}
}
$search-form-height: 70px;
.search-result-form {
height: $search-form-height;
padding-left: 10px;
padding-right: 10px;
border-bottom: 1px solid #222736;
}
.search-result-display {
overflow: auto;
height: calc(100% - #{$search-form-height});
}
.search-results {
// outline: 1px solid greenyellow;
margin-top: 10px; // &:first-of-type{
// margin-top: 10px;
padding-left: 10px; // margin-top: 10px;
padding-right: 10px; // margin-top: 10px;
// }
&__title {
text-transform: uppercase;
@ -44,18 +61,15 @@
padding: 5px;
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;
@ -68,12 +82,8 @@
.account {
display: block;
color: white;
border-radius: 2px;
transition: all .3s;
// &:hover &__name {
transition: all .3s; // &:hover &__name {
// text-decoration: underline;
// }
border-top: 1px solid $separator-color;
@ -92,18 +102,15 @@
&__fullhandle {
margin: 0 0 5px 0;
color: $status-secondary-color;
transition: all .3s;
// &:hover {
transition: all .3s; // &:hover {
// color: white;
// }
}
&:hover,
&:hover &__fullhandle {
color: white;
text-decoration: none;
background-color: $button-background-color-hover;
}
@include clearfix;
}

View File

@ -44,7 +44,7 @@ $inner-column-size: 320px;
.hashtag-stream {
display: block;
height: calc(100% - #{$hashtag-header-height} - 30px);
height: calc(100% - #{$hashtag-header-height});
width: $inner-column-size;
// outline: 1px greenyellow solid;
}

View File

@ -50,7 +50,7 @@ export class HashtagComponent implements OnInit {
event.stopPropagation();
const hashtag = this.hashtagElement.tag;
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.lastUsedAccount.id, hashtag, null, this.hashtagElement.displayableFullName);
const newStream = new StreamElement(StreamTypeEnum.tag, `${hashtag}`, this.lastUsedAccount.id, hashtag, null, this.lastUsedAccount.instance);
this.store.dispatch([new AddStream(newStream)]);
return false;

View File

@ -1,21 +1,32 @@
<div class="stream-overlay">
<div class="stream-overlay__header">
<a href class="overlay-close" (click)="close()">CLOSE</a>
<button class="overlay__button overlay-close" title="close" (click)="close()">
<fa-icon [icon]="faTimes"></fa-icon>
</button>
<button class="overlay__button overlay-previous" [ngClass]="{'overlay__button--focus': previousElements.length > 0 }" title="previous" (click)="previous()">
<fa-icon [icon]="faAngleLeft"></fa-icon>
</button>
<button class="overlay__button overlay-refresh" [ngClass]="{'overlay__button--focus': refreshFocused }" title="refresh" (click)="refresh()">
<fa-icon [icon]="faRedoAlt"></fa-icon>
</button>
<button class="overlay__button overlay-next" [ngClass]="{'overlay__button--focus': nextElements.length > 0 }" title="next" (click)="next()">
<fa-icon [icon]="faAngleRight"></fa-icon>
</button>
<!-- <a href class="overlay-close" (click)="close()">CLOSE</a>
<a href class="overlay-previous" (click)="previous()">PREV</a>
<a href class="overlay-refresh" *ngIf="canRefresh" (click)="refresh()">REFRESH</a>
<a href class="overlay-next" *ngIf="canGoForward" (click)="next()">NEXT</a>
<a href class="overlay-next" *ngIf="canGoForward" (click)="next()">NEXT</a> -->
</div>
<app-user-profile #appUserProfile *ngIf="accountName" [currentAccount]="accountName"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
<app-user-profile #appUserProfile *ngIf="accountName" [currentAccount]="accountName" class="stream-overlay__content"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-user-profile>
<app-hashtag #appHashtag *ngIf="hashtagElement" [hashtagElement]="hashtagElement"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
<app-hashtag #appHashtag *ngIf="hashtagElement" [hashtagElement]="hashtagElement" class="stream-overlay__content"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-hashtag>
<app-thread #appThread *ngIf="browseThread" [currentThread]="thread"
(browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)"
<app-thread #appThread *ngIf="browseThread" [currentThread]="thread" class="stream-overlay__content"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)"
(browseThreadEvent)="browseThread($event)"></app-thread>
</div>

View File

@ -1,20 +1,27 @@
@import "variables";
@import "mixins";
@import "commons";
$header-content: 40px;
.stream-overlay {
// width: $stream-column-width;
height: calc(100%);
background-color: $column-color;
&__header {
width: calc(100%);
height: 30px;
background-color: $column-header-background-color;
padding: 6px 10px 0 10px;
height: $header-content;
background-color: $column-header-background-color; // padding: 6px 10px 0 10px;
border-bottom: 1px solid #222736;
& a {
color: whitesmoke;
font-size: 0.8em;
font-weight: normal;
}
}
&__content {
display: block;
width: calc(100%);
height: calc(100% - #{$header-content});
}
&__title {
width: calc(100%);
height: 30px;
@ -27,23 +34,45 @@
.overlay {
margin: 0;
&__button {
@include clearButton;
width: 25px;
height: 25px;
color: #354060;
transition: all .2s;
margin: 8px 0 0 8px;
&:hover {
color: whitesmoke;
}
&--focus {
color: whitesmoke;
}
}
&-previous {
display: block;
float: left;
font-size: 18px;
& fa-icon {
position: relative;
left: -1px;
}
}
&-refresh {
display: block;
float: left;
margin-left: 65px;
font-size: 14px;
}
&-next {
display: block;
float: right;
padding-right: 20px;
float: left;
font-size: 18px;
}
&-close {
display: block;
float: right;
font-size: 14px;
color: white;
margin-right: 8px;
}
}

View File

@ -1,28 +1,37 @@
import { Component, OnInit, Output, EventEmitter, Input, ViewChild } from '@angular/core';
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ViewChild } from '@angular/core';
import { faAngleLeft, faAngleRight, faTimes, faRedoAlt } from "@fortawesome/free-solid-svg-icons";
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
import { ThreadComponent } from '../thread/thread.component';
import { UserProfileComponent } from '../user-profile/user-profile.component';
import { HashtagComponent } from '../hashtag/hashtag.component';
import { AccountInfo } from '../../../states/accounts.state';
@Component({
selector: 'app-stream-overlay',
templateUrl: './stream-overlay.component.html',
styleUrls: ['./stream-overlay.component.scss']
})
export class StreamOverlayComponent implements OnInit {
export class StreamOverlayComponent implements OnInit, OnDestroy {
faAngleLeft = faAngleLeft;
faAngleRight = faAngleRight;
faTimes = faTimes;
faRedoAlt = faRedoAlt;
private previousElements: OverlayBrowsing[] = [];
private nextElements: OverlayBrowsing[] = [];
refreshFocused: boolean;
previousElements: OverlayBrowsing[] = [];
nextElements: OverlayBrowsing[] = [];
private currentElement: OverlayBrowsing;
canRefresh: boolean = true;
canGoForward: boolean;
// canRefresh: boolean = true;
// canGoForward: boolean;
accountName: string;
thread: OpenThreadEvent;
// hashtag: string;
hashtagElement: StreamElement;
@Output() closeOverlay = new EventEmitter();
@ -30,7 +39,6 @@ export class StreamOverlayComponent implements OnInit {
@Input('browseAccountData')
set browseAccountData(accountName: string) {
this.browseAccount(accountName);
// this.accountName = accountName;
}
@Input('browseThreadData')
@ -47,9 +55,30 @@ export class StreamOverlayComponent implements OnInit {
@ViewChild('appHashtag') appHashtag: HashtagComponent;
@ViewChild('appThread') appThread: ThreadComponent;
constructor(private readonly toolsService: ToolsService) { }
private currentlyUsedAccount: AccountInfo;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
constructor(
private readonly store: Store,
private readonly toolsService: ToolsService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkAccountChanges(accounts);
});
}
checkAccountChanges(accounts: AccountInfo[]): any {
const selectedAccount = accounts.filter(x => x.isSelected)[0];
this.refreshFocused = selectedAccount.id !== this.currentlyUsedAccount.id;
}
ngOnDestroy() {
this.accountSub.unsubscribe();
}
close(): boolean {
@ -69,7 +98,8 @@ export class StreamOverlayComponent implements OnInit {
const nextElement = this.nextElements.pop();
this.loadElement(nextElement);
if(this.nextElements.length === 0) this.canGoForward = false;
//if(this.nextElements.length === 0) this.canGoForward = false;
return false;
}
@ -86,11 +116,14 @@ export class StreamOverlayComponent implements OnInit {
const previousElement = this.previousElements.pop();
this.loadElement(previousElement);
this.canGoForward = true;
//this.canGoForward = true;
return false;
}
refresh(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.refreshFocused = false;
if(this.thread){
this.appThread.refresh();
} else if(this.hashtagElement){
@ -111,7 +144,7 @@ export class StreamOverlayComponent implements OnInit {
}
const newElement = new OverlayBrowsing(null, accountName, null);
this.loadElement(newElement);
this.canGoForward = false;
//this.canGoForward = false;
}
browseHashtag(hashtag: string): void {
@ -123,10 +156,10 @@ export class StreamOverlayComponent implements OnInit {
}
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
const hashTagElement = new StreamElement(StreamTypeEnum.tag, hashtag, selectedAccount.id, hashtag, null, `#${hashtag}@${selectedAccount.instance}`);
const hashTagElement = new StreamElement(StreamTypeEnum.tag, hashtag, selectedAccount.id, hashtag, null, selectedAccount.instance);
const newElement = new OverlayBrowsing(hashTagElement, null, null);
this.loadElement(newElement);
this.canGoForward = false;
// this.canGoForward = false;
}
browseThread(openThread: OpenThreadEvent): any {
@ -139,10 +172,13 @@ export class StreamOverlayComponent implements OnInit {
const newElement = new OverlayBrowsing(null, null, openThread);
this.loadElement(newElement);
this.canGoForward = false;
//this.canGoForward = false;
}
private loadElement(element: OverlayBrowsing) {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.refreshFocused = false;
this.currentElement = element;
this.accountName = this.currentElement.account;

View File

@ -1,5 +1,5 @@
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<app-waiting-animation *ngIf="statuses.length === 0" class="waiting-icon"></app-waiting-animation>
<div *ngIf="displayError" class="stream-toots__error">{{displayError}}</div>

View File

@ -1,25 +1,27 @@
<div class="stream-column">
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()"
[browseAccountData]="overlayAccountToBrowse"
[browseHashtagData]="overlayHashtagToBrowse"
[browseAccountData]="overlayAccountToBrowse" [browseHashtagData]="overlayHashtagToBrowse"
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
<div class="stream-column__stream-header">
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">
<fa-icon class="stream-column__open-menu--icon" [icon]="menuFaIcon"></fa-icon>
</a>
<a class="stream-column__stream-selector" href title="return to top" (click)="goToTop()">
<fa-icon class="stream-column__stream-selector--icon" [icon]="columnFaIcon"></fa-icon>
<h1 class="stream-column__stream-selector--title">{{ streamElement.name.toUpperCase() }}</h1>
<span class="stream-column__stream-selector--subtitle"
*ngIf="streamElement.instance">{{ streamElement.instance.toLowerCase() }}</span>
</a>
<a class="stream-column__open-menu" href title="edit column" (click)="openEditionMenu()">
<fa-icon class="stream-column__open-menu--icon" [icon]="menuFaIcon"></fa-icon>
</a>
</div>
<app-stream-edition class="stream-edition" *ngIf="editionPanelIsOpen"
[streamElement]="streamElement"></app-stream-edition>
<app-stream-edition class="stream-edition" *ngIf="editionPanelIsOpen" [streamElement]="streamElement">
</app-stream-edition>
<app-stream-statuses class="stream-statuses" [streamElement]="streamElement" [goToTop]="goToTopSubject.asObservable()"
(browseAccountEvent)="browseAccount($event)" (browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-stream-statuses>
<app-stream-statuses class="stream-statuses" [streamElement]="streamElement"
[goToTop]="goToTopSubject.asObservable()" (browseAccountEvent)="browseAccount($event)"
(browseHashtagEvent)="browseHashtag($event)" (browseThreadEvent)="browseThread($event)"></app-stream-statuses>
<!-- <div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-status>

View File

@ -17,17 +17,20 @@ $stream-header-height: 40px;
border-bottom: 1px solid #222736;
}
&__open-menu {
float: right;
display: block;
width: $stream-header-height - 10px;
height: $stream-header-height - 10px;
margin: 5px;
position: absolute;
top: 5px;
right: 5px;
&:hover &--icon {
color: darken(whitesmoke, 30);
}
&--icon {
color: whitesmoke; // float: left;
position: relative;
// position: relative;
position: absolute;
top: 4px;
left: 8px;
}
@ -37,20 +40,30 @@ $stream-header-height: 40px;
width: calc(100%);
height: $stream-header-height;
background-color: $column-header-background-color;
text-decoration: none; // &:hover {
// }
text-decoration: none;
color: whitesmoke;
position: relative;
&--icon {
color: whitesmoke;
float: left;
position: relative;
left: 11px;
top: 9px;
}
&--title {
color: whitesmoke;
font-size: 0.8em;
font-weight: normal; // margin: 0 0 0 25px;
padding: 14px 0 0 35px;
font-weight: normal;
position: absolute;
top: 9px;
left: 35px;
}
&--subtitle {
color: $font-link-primary-hover;
font-size: 0.7em;
font-weight: normal;
font-style: italic;
position: absolute;
top: 21px;
left: 35px;;
}
}
}

View File

@ -1,22 +1,53 @@
<div class="profile ">
<div class="profile flexcroll">
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
<div class="profile-sub-header flexcroll">
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
<div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
</div>
</div>
<div *ngIf="account && hasNote" class="profile-description">
<app-databinded-text class="status__content" [textIsSelectable]="false" [text]="account.note"
(accountSelected)="browseAccount($event)"
(hashtagSelected)="browseHashtag($event)"></app-databinded-text>
<!-- <p innerHTML="{{account.note}}"></p> -->
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
<div class="profile-header__inner">
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
<div class="profile-header__follow" *ngIf="relationship">
<button class="profile-header__follow--button profile-header__follow--unfollowed" title="follow"
(click)="follow()" *ngIf="!relationship.following && !relationship.requested">
<fa-icon [icon]="faUserRegular"></fa-icon>
</button>
<button class="profile-header__follow--button profile-header__follow--followed" title="unfollow"
(click)="unfollow()" *ngIf="relationship.following">
<fa-icon [icon]="faUserCheck"></fa-icon>
</button>
<button class="profile-header__follow--button profile-header__follow--followed" title="pending"
(click)="unfollow()" *ngIf="relationship.requested">
<fa-icon [icon]="faHourglassHalf"></fa-icon>
</button>
</div>
<div class="profile-header__state" *ngIf="relationship">
<div class="profile-header__state--data" *ngIf="relationship.followed_by">follows you</div>
<div class="profile-header__state--data" *ngIf="relationship.blocking">blocked</div>
<div class="profile-header__state--data" *ngIf="relationship.muting">muted</div>
</div>
</div>
</div>
<div class="profile-sub-header ">
<div *ngIf="account && hasNote" class="profile-description">
<!-- <div *ngIf="account && account.note" class="profile-description"> -->
<app-databinded-text class="profile-description__content" [textIsSelectable]="false" [text]="account.note"
(accountSelected)="browseAccount($event)" (hashtagSelected)="browseHashtag($event)">
</app-databinded-text>
</div>
<div class="profile-fields" *ngIf="account && account.fields.length > 0">
<div class="profile-fields__field" *ngFor="let field of account.fields">
<div class="profile-fields__field--value" innerHTML="{{field.value}}" [ngClass]="{'profile-fields__field--validated': field.verified_at }">
</div>
<div class="profile-fields__field--name">
{{ field.name }}
</div>
</div>
</div>
<div class="profile-statuses">
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
@ -25,10 +56,9 @@
</div>
<div *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper"
(browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)"
(browseThreadEvent)="browseThread($event)"></app-status>
<app-status [statusWrapper]="statusWrapper" (browseHashtagEvent)="browseHashtag($event)"
(browseAccountEvent)="browseAccount($event)" (browseThreadEvent)="browseThread($event)">
</app-status>
</div>
</div>
</div>

View File

@ -1,12 +1,13 @@
@import "variables";
@import "mixins";
@import "commons";
$validated-font-color: #4fde23;
$validated-background: #164109;
$header-height: 160px;
.profile {
// overflow: auto;
height: calc(100% - 30px);
height: calc(100%);
overflow: auto;
&-header {
background-size: cover;
position: relative; // height: 140px;
@ -31,34 +32,131 @@ $header-height: 160px;
position: absolute;
top: 105px;
left: 15px;
width: calc(100% - 30px);
overflow: hidden;
color: white;
}
&__fullhandle a {
position: absolute;
top: 130px;
left: 15px;
width: calc(100% - 30px);
overflow: hidden;
color: white;
}
&__follow {
// transition: all .4s;
position: absolute;
top: 10px;
right: 15px;
font-size: 28px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
&--button {
@include clearButton;
}
&--unfollowed {}
&--followed {
color: #38abff;
color: #5fbcff;
color: #85ccff;
}
}
&__state {
position: absolute;
top: 50px;
right: 15px;
font-size: 12px;
&--data {
text-align: right;
}
}
}
&-sub-header {
overflow: auto;
height: calc(100% - #{$header-height});
height: calc(100%);
// overflow: auto;
// height: calc(100% - #{$header-height});
// height: calc(100%);
// height: calc(20% - 190px);
// height: 150px;
// border: 1px solid greenyellow;
}
&-description {
padding: 10px 10px 15px 10px;
font-size: 13px;
border-bottom: 1px solid black;
&__content {
width: calc(100%);
word-break: break-word;
}
}
&-fields {
font-size: 13px;
border-bottom: 1px solid black;
&__field {
&:not(:last-child) {
border-bottom: 1px solid black;
}
&--name {
padding: 10px;
border-right: 1px solid black;
text-align: center;
width: calc(33%);
background-color: #0b0d13;
white-space: nowrap;
overflow: hidden;
}
&--value {
padding: 10px;
width: calc(66%);
float: right;
white-space: nowrap;
}
&--validated {
background-color: $validated-background;
// border: 1px solid $validated-font-color;
}
}
@include clearfix;
}
&-no-toots {
text-align: center;
margin: 15px;
border: 2px solid whitesmoke;
}
}
//Mastodon styling
:host ::ng-deep .profile-fields__field--value {
// font-size: 14px;
color: $status-primary-color;
& a,
.mention,
.ellipsis {
color: $status-links-color;
}
& .invisible {
display: none;
}
& p {
margin: 0px; //font-size: .9em;
// font-size: 14px;
}
}
:host ::ng-deep .profile-fields__field--validated {
// font-size: 14px;
color: $validated-font-color;
& a,
.mention,
.ellipsis {
color: $validated-font-color;
}
& .invisible {
display: none;
}
& p {
margin: 0px; //font-size: .9em;
// font-size: 14px;
}
}

View File

@ -1,11 +1,18 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { faUser, faHourglassHalf, faUserCheck } from "@fortawesome/free-solid-svg-icons";
import { faUser as faUserRegular } from "@fortawesome/free-regular-svg-icons";
import { Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { Account, Status } from "../../../services/models/mastodon.interfaces";
import { Account, Status, Relationship } from "../../../services/models/mastodon.interfaces";
import { MastodonService } from '../../../services/mastodon.service';
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
import { StatusWrapper } from '../stream.component';
import { NotificationService } from '../../../services/notification.service';
import { AccountInfo } from '../../../states/accounts.state';
@Component({
selector: 'app-user-profile',
@ -13,6 +20,10 @@ import { NotificationService } from '../../../services/notification.service';
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
faUser = faUser;
faUserRegular = faUserRegular;
faHourglassHalf = faHourglassHalf;
faUserCheck = faUserCheck;
account: Account;
hasNote: boolean;
@ -21,38 +32,77 @@ export class UserProfileComponent implements OnInit {
statusLoading: boolean;
error: string;
relationship: Relationship;
statuses: StatusWrapper[] = [];
private lastAccountName: string;
private currentlyUsedAccount: AccountInfo;
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
@Output() browseAccountEvent = new EventEmitter<string>();
@Output() browseHashtagEvent = new EventEmitter<string>();
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
@Input('currentAccount')
//set currentAccount(account: Account) {
set currentAccount(accountName: string) {
this.lastAccountName = accountName;
this.load(this.lastAccountName);
this.load(accountName);
}
constructor(
private readonly store: Store,
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly toolsService: ToolsService) { }
private readonly toolsService: ToolsService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (this.account) {
this.currentlyUsedAccount = accounts.filter(x => x.isSelected)[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
.then((account: Account) => {
this.getFollowStatus(this.currentlyUsedAccount, account);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
}
});
}
ngOnDestroy() {
this.accountSub.unsubscribe();
}
private load(accountName: string) {
this.statuses.length = 0;
this.account = null;
this.isLoading = true;
this.loadAccount(accountName)
this.lastAccountName = accountName;
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
return this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
.then((account: Account) => {
console.warn(account);
this.isLoading = false;
this.statusLoading = true;
this.account = account;
this.hasNote = account && account.note && account.note !== '<p></p>';
return this.getStatuses(this.account);
const getFollowStatusPromise = this.getFollowStatus(this.currentlyUsedAccount, this.account);
const getStatusesPromise = this.getStatuses(this.currentlyUsedAccount, this.account);
return Promise.all([getFollowStatusPromise, getStatusesPromise]);
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
@ -63,6 +113,26 @@ export class UserProfileComponent implements OnInit {
});
}
private getStatuses(userAccount: AccountInfo, account: Account): Promise<void> {
this.statusLoading = true;
return this.mastodonService.getAccountStatuses(userAccount, account.id, false, false, true, null, null, 40)
.then((result: Status[]) => {
for (const status of result) {
const wrapper = new StatusWrapper(status, userAccount);
this.statuses.push(wrapper);
}
this.statusLoading = false;
});
}
private getFollowStatus(userAccount: AccountInfo, account: Account): Promise<void> {
// this.relationship = null;
return this.mastodonService.getRelationships(userAccount, [account])
.then((result: Relationship[]) => {
this.relationship = result.filter(x => x.id === account.id)[0];
});
}
refresh(): any {
this.load(this.lastAccountName);
}
@ -79,37 +149,33 @@ export class UserProfileComponent implements OnInit {
this.browseThreadEvent.next(openThreadEvent);
}
private loadAccount(accountName: string): Promise<Account> {
this.account = null;
let selectedAccounts = this.toolsService.getSelectedAccounts();
if (selectedAccounts.length === 0) {
this.error = 'no user selected';
console.error(this.error);
return Promise.resolve(null);
}
this.isLoading = true;
return this.toolsService.findAccount(selectedAccounts[0], accountName)
.then((result) => {
this.isLoading = false;
return result;
follow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
.then((account: Account) => {
return this.mastodonService.follow(this.currentlyUsedAccount, account);
})
.then((relationship: Relationship) => {
this.relationship = relationship;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
return false;
}
private getStatuses(account: Account): Promise<void> {
let selectedAccounts = this.toolsService.getSelectedAccounts();
if (selectedAccounts.length === 0) return;
this.statusLoading = true;
return this.mastodonService.getAccountStatuses(selectedAccounts[0], account.id, false, false, true, null, null, 40)
.then((result: Status[]) => {
for (const status of result) {
const wrapper = new StatusWrapper(status, selectedAccounts[0]);
this.statuses.push(wrapper);
}
this.statusLoading = false;
unfollow(): boolean {
this.currentlyUsedAccount = this.toolsService.getSelectedAccounts()[0];
this.toolsService.findAccount(this.currentlyUsedAccount, this.lastAccountName)
.then((account: Account) => {
return this.mastodonService.unfollow(this.currentlyUsedAccount, account);
})
.then((relationship: Relationship) => {
this.relationship = relationship;
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err);
});
return false;
}
}

View File

@ -1,5 +1,5 @@
<div class="streams-selection-footer">
<a class="stream-selection" *ngFor="let str of streams; let i=index" href (click)="onColumnSelection(i)" title="open {{str.displayableFullName}}">
<a class="stream-selection" *ngFor="let str of streams; let i=index" href (click)="onColumnSelection(i)" title="open {{getDisplayableName(str)}}">
<span class="stream-selection__column-reprensentation"></span>
</a>
</div>

View File

@ -1,32 +1,54 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { StreamElement } from '../../states/streams.state';
import { StreamElement, StreamTypeEnum } from '../../states/streams.state';
import { Store } from '@ngxs/store';
import { NavigationService } from '../../services/navigation.service';
@Component({
selector: 'app-streams-selection-footer',
templateUrl: './streams-selection-footer.component.html',
styleUrls: ['./streams-selection-footer.component.scss']
selector: 'app-streams-selection-footer',
templateUrl: './streams-selection-footer.component.html',
styleUrls: ['./streams-selection-footer.component.scss']
})
export class StreamsSelectionFooterComponent implements OnInit {
streams: StreamElement[] = [];
private streams$: Observable<StreamElement[]>;
streams: StreamElement[] = [];
private streams$: Observable<StreamElement[]>;
constructor(
private readonly navigationService: NavigationService,
private readonly store: Store) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
}
constructor(
private readonly navigationService: NavigationService,
private readonly store: Store) {
this.streams$ = this.store.select(state => state.streamsstatemodel.streams);
}
ngOnInit() {
this.streams$.subscribe((streams: StreamElement[]) => {
this.streams = streams;
});
}
ngOnInit() {
this.streams$.subscribe((streams: StreamElement[]) => {
this.streams = streams;
});
}
onColumnSelection(index: number): boolean {
this.navigationService.columnSelected(index);
return false;
}
onColumnSelection(index: number): boolean {
this.navigationService.columnSelected(index);
return false;
}
getDisplayableName(stream: StreamElement): string {
let prefix = '';
switch (stream.type) {
case StreamTypeEnum.local:
prefix = "local";
break;
case StreamTypeEnum.personnal:
prefix = "home";
break;
case StreamTypeEnum.global:
prefix = "federated";
break;
case StreamTypeEnum.tag:
prefix = `#${stream.tag}`;
break;
case StreamTypeEnum.list:
prefix = `${stream.list}`;
break;
}
return `${prefix}@${stream.instance}`;
}
}

View File

@ -13,7 +13,7 @@
<div class="open-account__mouse-icon"></div>
<h3 class="open-account__title">Nice!</h3>
<p class="open-account__description">
Now <span class="underline">left-click</span> on your avatar to open your account and be able to add some timelines!
Now <span class="underline">right-click</span> on your avatar to open your account and be able to add some timelines!
</p>
</div>
<!-- </div> -->

View File

@ -2,14 +2,14 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context } from "./models/mastodon.interfaces";
import { Account, Status, Results, Context, Relationship } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum } from '../states/streams.state';
import { stat } from 'fs';
import { forEach } from '@angular/router/src/utils/collection';
@Injectable()
export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { }
@ -166,6 +166,32 @@ export class MastodonService {
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
}
getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise<Relationship[]> {
let params = "?";
accountsToRetrieve.forEach(x => {
if(params.includes('id')) params += '&';
params += `id[]=${x.id}`;
});
const route = `https://${account.instance}${this.apiRoutes.getAccountRelationships}${params}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Relationship[]>(route, { headers: headers }).toPromise();
}
follow(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
unfollow(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.unfollow}`.replace('{0}', account.id.toString());
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
}
}
export enum VisibilityEnum {

View File

@ -1,136 +1,153 @@
export interface AppData {
client_id: string;
client_secret: string;
id: string;
name: string;
redirect_uri: string;
website: string;
client_id: string;
client_secret: string;
id: string;
name: string;
redirect_uri: string;
website: string;
}
export interface TokenData {
access_token: string;
token_type: string;
scope: string;
created_at: string;
access_token: string;
token_type: string;
scope: string;
created_at: string;
}
export interface Account {
id: number;
username: string;
acct: string;
display_name: string;
locked: string;
created_at: string;
followers_count: number;
following_count: number;
statuses_count: number;
note: string;
url: string;
avatar: string;
avatar_static: string;
header: string;
header_static: string;
id: number;
username: string;
acct: string;
display_name: string;
locked: string;
created_at: string;
followers_count: number;
following_count: number;
statuses_count: number;
note: string;
url: string;
avatar: string;
avatar_static: string;
header: string;
header_static: string;
emojis: Emoji[];
moved: boolean;
fields: Field[];
bot: boolean;
}
export interface Emoji {
shortcode: string;
static_url: string;
url: string;
visible_in_picker: boolean;
}
export interface Field {
name: string;
value: string;
verified_at: string;
}
export interface Application {
name: string;
website: string;
name: string;
website: string;
}
export interface Attachment {
id: string;
type: 'image' | 'video' | 'gifv';
url: string;
remote_url: string;
preview_url: string;
text_url: string;
id: string;
type: 'image' | 'video' | 'gifv';
url: string;
remote_url: string;
preview_url: string;
text_url: string;
}
export interface Card {
url: string;
title: string;
description: string;
image: string;
url: string;
title: string;
description: string;
image: string;
}
export interface Context {
ancestors: Status[];
descendants: Status[];
ancestors: Status[];
descendants: Status[];
}
export interface Error {
error: string;
error: string;
}
export interface Instance {
uri: string;
title: string;
description: string;
email: string;
uri: string;
title: string;
description: string;
email: string;
}
export interface Mention {
url: string;
username: string;
acct: string;
id: string;
url: string;
username: string;
acct: string;
id: string;
}
export interface Notification {
id: string;
type: 'mention' | 'reblog' | 'favourite' | 'follow';
created_at: string;
account: Account;
status?: Status;
id: string;
type: 'mention' | 'reblog' | 'favourite' | 'follow';
created_at: string;
account: Account;
status?: Status;
}
export interface Relationship {
id: string;
following: string;
followed_by: string;
blocking: string;
muting: string;
requested: string;
id: number;
following: boolean;
followed_by: boolean;
blocking: boolean;
muting: boolean;
requested: boolean;
}
export interface Report {
id: string;
action_taken: boolean;
id: string;
action_taken: boolean;
}
export interface Results {
accounts: Account[];
statuses: Status[];
hashtags: string[];
accounts: Account[];
statuses: Status[];
hashtags: string[];
}
export interface Status {
id: string;
uri: string;
url: string;
account: Account;
in_reply_to_id: string;
in_reply_to_account_id: string;
reblog: Status;
content: string;
created_at: string;
reblogs_count: string;
favourites_count: string;
reblogged: boolean;
favourited: boolean;
sensitive: boolean;
spoiler_text: string;
visibility: string;
media_attachments: Attachment[];
mentions: Mention[];
tags: Tag[];
application: Application;
emojis: any[];
language: string;
pinned: boolean;
id: string;
uri: string;
url: string;
account: Account;
in_reply_to_id: string;
in_reply_to_account_id: string;
reblog: Status;
content: string;
created_at: string;
reblogs_count: string;
favourites_count: string;
reblogged: boolean;
favourited: boolean;
sensitive: boolean;
spoiler_text: string;
visibility: string;
media_attachments: Attachment[];
mentions: Mention[];
tags: Tag[];
application: Application;
emojis: any[];
language: string;
pinned: boolean;
}
export interface Tag {
name: string;
url: string;
name: string;
url: string;
}

View File

@ -16,7 +16,8 @@ export class NotificationService {
public notifyHttpError(err: HttpErrorResponse){
console.error(err.message);
let message = `${err.status}: ${err.statusText}`;
// let message = `${err.status}: ${err.statusText}`;
let message = `${err.statusText}`;
this.notify(message, true);
}
}

View File

@ -98,7 +98,7 @@ export class StreamElement {
public accountId: string,
public tag: string,
public list: string,
public displayableFullName: string) {
public instance: string) {
this.id = `${type}-${name}-${accountId}`;
}
}

View File

@ -5,6 +5,7 @@
padding: 10px 10px 0 7px;
font-size: $small-font-size;
white-space: normal;
// overflow: auto;
&__title {
font-size: 13px;
text-transform: uppercase;