Merge pull request #25 from NicolasConstant/feature_handle-hashtags
Feature handle hashtags
This commit is contained in:
commit
74a8dd21f1
|
@ -41,6 +41,7 @@ import { HashtagComponent } from './components/stream/hashtag/hashtag.component'
|
|||
import { StreamOverlayComponent } from './components/stream/stream-overlay/stream-overlay.component';
|
||||
import { DatabindedTextComponent } from './components/stream/status/databinded-text/databinded-text.component';
|
||||
import { TimeAgoPipe } from './pipes/time-ago.pipe';
|
||||
import { StreamStatusesComponent } from './components/stream/stream-statuses/stream-statuses.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
|
@ -74,7 +75,8 @@ const routes: Routes = [
|
|||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe
|
||||
TimeAgoPipe,
|
||||
StreamStatusesComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<div class="floating-column">
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">x</a>
|
||||
</div>
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive" (closeOverlay)="closeOverlay()" [browseAccount]="overlayAccountToBrowse" [browseHashtag]="overlayHashtagToBrowse"></app-stream-overlay>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'"></app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">x</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"></app-manage-account>
|
||||
<app-add-new-status *ngIf="openPanel === 'createNewStatus'"></app-add-new-status>
|
||||
<app-add-new-account *ngIf="openPanel === 'addNewAccount'"></app-add-new-account>
|
||||
<app-search *ngIf="openPanel === 'search'" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-search>
|
||||
<app-settings *ngIf="openPanel === 'settings'"></app-settings>
|
||||
</div>
|
|
@ -1,9 +1,11 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
$floating-column-size: 330px;
|
||||
|
||||
.floating-column {
|
||||
width: calc(100%);
|
||||
max-width: 330px;
|
||||
width: $floating-column-size;
|
||||
|
||||
background-color: $color-secondary;
|
||||
overflow: hidden;
|
||||
|
@ -13,12 +15,17 @@
|
|||
bottom: $stream-selector-height;
|
||||
padding: 0;
|
||||
|
||||
&__header {
|
||||
// &__header {
|
||||
|
||||
// @include clearfix;
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
.stream-overlay {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
width: $floating-column-size;
|
||||
height: calc(100%);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
// display: inline-block;
|
||||
|
|
|
@ -8,11 +8,14 @@ import { AccountWrapper } from '../../models/account.models';
|
|||
styleUrls: ['./floating-column.component.scss']
|
||||
})
|
||||
export class FloatingColumnComponent implements OnInit {
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: string;
|
||||
overlayHashtagToBrowse: string;
|
||||
|
||||
userAccountUsed: AccountWrapper;
|
||||
|
||||
openPanel: string;
|
||||
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -48,4 +51,26 @@ export class FloatingColumnComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
browseAccount(account: string): void {
|
||||
this.overlayAccountToBrowse = account;
|
||||
this.overlayHashtagToBrowse = null;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
this.overlayAccountToBrowse = null;
|
||||
this.overlayHashtagToBrowse = hashtag;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseThread(thread: string): void {
|
||||
console.warn('browseThread');
|
||||
console.warn(thread);
|
||||
}
|
||||
|
||||
closeOverlay(): boolean {
|
||||
this.overlayActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ export class ManageAccountComponent implements OnInit {
|
|||
|
||||
ngOnInit() {
|
||||
this.availableStreams.length = 0;
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Global Timeline', this.account.info.id));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Personnal Timeline', this.account.info.id));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.global, 'Global Timeline', this.account.info.id, null, null));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.local, 'Local Timeline', this.account.info.id, null, null));
|
||||
this.availableStreams.push(new StreamElement(StreamTypeEnum.personnal, 'Personnal Timeline', this.account.info.id, null, null));
|
||||
}
|
||||
|
||||
addStream(stream: StreamElement): boolean {
|
||||
|
|
|
@ -1,31 +1,36 @@
|
|||
<div class="panel">
|
||||
<h3 class="panel__title">search</h3>
|
||||
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<input type="text" class="form-control form-control-sm" [(ngModel)]="searchHandle" name="searchHandle"
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<a href *ngFor="let account of accounts" class="account" title="open account"
|
||||
(click)="selectAccount(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 *ngIf="statuses.length > 0" class="search-results">
|
||||
<h3 class="search-results__title">Statuses</h3>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hashtags.length > 0" class="search-results">
|
||||
<h3 class="search-results__title">Hashtags</h3>
|
||||
<a href *ngFor="let hashtag of hashtags" class="search-results__hashtag">
|
||||
<a (click)="selectHashtag(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" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-status>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -3,66 +3,105 @@
|
|||
@import "panel";
|
||||
@import "commons";
|
||||
|
||||
$separator-color:$color-primary;
|
||||
|
||||
.form-section {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-with-button {
|
||||
width: calc(100% - #{$button-size});
|
||||
float: left;
|
||||
}
|
||||
|
||||
.form-button {
|
||||
width: $button-size;
|
||||
height: 29px;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: $button-background-color;
|
||||
color: $button-color;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
color: $button-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
// outline: 1px solid greenyellow;
|
||||
margin-top: 10px;
|
||||
|
||||
// &:first-of-type{
|
||||
margin-top: 10px; // &:first-of-type{
|
||||
// margin-top: 10px;
|
||||
// }
|
||||
|
||||
&__title {
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&__hashtag {
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
|
||||
border-top: 1px solid $separator-color;
|
||||
&:last-of-type{
|
||||
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 {
|
||||
border-top: 1px solid $separator-color;
|
||||
&:last-of-type {
|
||||
border-bottom: 1px solid $separator-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account {
|
||||
display: block;
|
||||
color: white;
|
||||
display: block;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
border-radius: 2px;
|
||||
transition: all .3s;
|
||||
|
||||
&:hover &__name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
border-top: 1px solid $separator-color;
|
||||
&:last-of-type{
|
||||
|
||||
// &:hover &__name {
|
||||
// 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;
|
||||
margin: 7px 0 0 0;
|
||||
}
|
||||
|
||||
&__fullhandle {
|
||||
margin: 0 0 5px 0;
|
||||
color: $status-secondary-color;
|
||||
transition: all .3s;
|
||||
// &:hover {
|
||||
// color: white;
|
||||
// }
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover &__fullhandle {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: $button-background-color-hover;
|
||||
}
|
||||
|
||||
@include clearfix;
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { Results, Account, Status } from '../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../stream/stream.component';
|
||||
import { StreamElement, StreamTypeEnum, AddStream } from './../../../states/streams.state';
|
||||
|
||||
|
||||
@Component({
|
||||
|
@ -15,13 +18,18 @@ export class SearchComponent implements OnInit {
|
|||
@Input() searchHandle: string;
|
||||
|
||||
accounts: Account[] = [];
|
||||
statuses: Status[] = [];
|
||||
statuses: StatusWrapper[] = [];
|
||||
hashtags: string[] = [];
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
@Output() browseThread = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -33,6 +41,31 @@ export class SearchComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
|
||||
selectHashtag(hashtag: string): boolean {
|
||||
if (hashtag) {
|
||||
this.browseHashtag.next(hashtag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// addHashtag(hashtag: string): boolean {
|
||||
// if (hashtag) {
|
||||
// const newStream = new StreamElement(StreamTypeEnum.tag, `#${hashtag}`, this.lastAccountUsed.id, hashtag, null);
|
||||
// this.store.dispatch([new AddStream(newStream)]);
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// }
|
||||
|
||||
selectAccount(accountName: string): boolean {
|
||||
console.warn(accountName);
|
||||
if (accountName) {
|
||||
this.browseAccount.next(accountName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private lastAccountUsed: AccountInfo;
|
||||
private search(data: string) {
|
||||
this.accounts.length = 0;
|
||||
this.statuses.length = 0;
|
||||
|
@ -41,23 +74,23 @@ export class SearchComponent implements OnInit {
|
|||
|
||||
console.warn(`search: ${data}`);
|
||||
|
||||
const enabledAccounts = this.getRegisteredAccounts().filter(x => x.isSelected);
|
||||
|
||||
const enabledAccounts = this.toolsService.getSelectedAccounts();
|
||||
//First candid implementation
|
||||
if (enabledAccounts.length > 0) {
|
||||
const candid_oneAccount = enabledAccounts[0];
|
||||
this.mastodonService.search(candid_oneAccount, data, true)
|
||||
this.lastAccountUsed = enabledAccounts[0];
|
||||
this.mastodonService.search(this.lastAccountUsed, data, true)
|
||||
.then((results: Results) => {
|
||||
if (results) {
|
||||
console.warn(results);
|
||||
this.accounts = results.accounts;
|
||||
//this.statuses = results.statuses;
|
||||
this.accounts = results.accounts.slice(0, 5);
|
||||
this.hashtags = results.hashtags;
|
||||
|
||||
//TODO: Pleroma return more than mastodon, will have to handle that
|
||||
if (this.accounts.length > 5) {
|
||||
this.accounts.length = 5;
|
||||
for (let status of results.statuses) {
|
||||
const statusWrapper = new StatusWrapper(status, this.lastAccountUsed);
|
||||
this.statuses.push(statusWrapper);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
<p>
|
||||
hashtag works!
|
||||
</p>
|
||||
<div class="hashtag-column">
|
||||
<div class="hashtag-header">
|
||||
<a href (click)="goToTop()" class="hashtag-header__gototop" title="go to top">
|
||||
<h3 class="hashtag-header__title">#{{hashtagElement.tag}}</h3>
|
||||
|
||||
<button class="btn-custom-secondary hashtag-header__add-column" (click)="addColumn($event)" title="add column to board">add column</button>
|
||||
</a>
|
||||
</div>
|
||||
<app-stream-statuses class="hashtag-stream" *ngIf="hashtagElement" [streamElement]="hashtagElement" [goToTop]="goToTopSubject.asObservable()"
|
||||
(browseAccount)="selectAccount($event)" (browseHashtag)="selectHashtag($event)"></app-stream-statuses>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
@import "variables";
|
||||
@import "buttons";
|
||||
|
||||
$hashtag-header-height: 40px;
|
||||
$inner-column-size: 320px;
|
||||
|
||||
.hashtag-column{
|
||||
height: calc(100%);
|
||||
width: $inner-column-size;
|
||||
}
|
||||
|
||||
.hashtag-header {
|
||||
height: $hashtag-header-height;
|
||||
border-bottom: 1px solid black;
|
||||
position: relative;
|
||||
|
||||
&__gototop {
|
||||
display: block;
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
color: white;
|
||||
transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: $btn-primary-color-hover;
|
||||
// }
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1em;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
&__add-column {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
padding: 0 10px 0 10px;
|
||||
border: 1px solid black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.hashtag-stream {
|
||||
display: block;
|
||||
height: calc(100% - #{$hashtag-header-height} - 30px);
|
||||
width: $inner-column-size;
|
||||
// outline: 1px greenyellow solid;
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement, StreamTypeEnum, AddStream } from '../../../states/streams.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashtag',
|
||||
|
@ -6,19 +10,39 @@ import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
|||
styleUrls: ['./hashtag.component.scss']
|
||||
})
|
||||
export class HashtagComponent implements OnInit {
|
||||
hashtag: string;
|
||||
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
|
||||
@Input('currentHashtag')
|
||||
set currentAccount(hashtag: string) {
|
||||
this.hashtag = hashtag;
|
||||
}
|
||||
@Input() hashtagElement: StreamElement;
|
||||
|
||||
constructor() { }
|
||||
goToTopSubject: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private readonly store: Store) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
goToTop(): boolean {
|
||||
this.goToTopSubject.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
addColumn(event): boolean {
|
||||
event.stopPropagation();
|
||||
|
||||
const hashtag = this.hashtagElement.tag;
|
||||
const newStream = new StreamElement(StreamTypeEnum.tag, `#${hashtag}`, this.hashtagElement.accountId, hashtag, null);
|
||||
this.store.dispatch([new AddStream(newStream)]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
selectAccount(account: string) {
|
||||
this.browseAccount.next(account);
|
||||
}
|
||||
|
||||
selectHashtag(hashtag: string) {
|
||||
this.browseHashtag.next(hashtag);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
</div> -->
|
||||
|
||||
<app-user-profile *ngIf="accountName" [currentAccount]="accountName" (browseAccount)="accountSelected($event)" (browseHashtag)="hashtagSelected($event)"></app-user-profile>
|
||||
<app-hashtag *ngIf="browseHashtag"></app-hashtag>
|
||||
<app-hashtag *ngIf="hashtagElement" [hashtagElement]="hashtagElement" (browseAccount)="accountSelected($event)" (browseHashtag)="hashtagSelected($event)"></app-hashtag>
|
||||
<app-thread *ngIf="browseThread"></app-thread>
|
||||
</div>
|
|
@ -1,13 +1,9 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
.stream-overlay {
|
||||
// position: absolute;
|
||||
// z-index: 50;
|
||||
width: $stream-column-width;
|
||||
// width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
background-color: $column-color; // margin: 0 0 0 $stream-column-separator;
|
||||
// outline: 1px red solid;
|
||||
// float: left;
|
||||
background-color: $column-color;
|
||||
&__header {
|
||||
width: calc(100%);
|
||||
height: 30px;
|
||||
|
@ -16,7 +12,7 @@
|
|||
& a {
|
||||
color: whitesmoke;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal; // margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
|||
import { Account, Results } from "../../../services/models/mastodon.interfaces";
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { StreamElement, StreamTypeEnum } from '../../../states/streams.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-overlay',
|
||||
|
@ -18,7 +19,8 @@ export class StreamOverlayComponent implements OnInit {
|
|||
|
||||
accountName: string;
|
||||
thread: string;
|
||||
hashtag: string;
|
||||
// hashtag: string;
|
||||
hashtagElement: StreamElement;
|
||||
|
||||
@Output() closeOverlay = new EventEmitter();
|
||||
|
||||
|
@ -39,7 +41,7 @@ export class StreamOverlayComponent implements OnInit {
|
|||
// this.hashtag = hashtag;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
constructor(private toolsService: ToolsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
@ -63,6 +65,7 @@ export class StreamOverlayComponent implements OnInit {
|
|||
const nextElement = this.nextElements.pop();
|
||||
this.loadElement(nextElement);
|
||||
|
||||
if(this.nextElements.length === 0) this.canGoForward = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -111,7 +114,10 @@ export class StreamOverlayComponent implements OnInit {
|
|||
if (this.currentElement) {
|
||||
this.previousElements.push(this.currentElement);
|
||||
}
|
||||
const newElement = new OverlayBrowsing(hashtag, null, null);
|
||||
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
const hashTagElement = new StreamElement(StreamTypeEnum.tag, hashtag, selectedAccount.id, hashtag, null);
|
||||
const newElement = new OverlayBrowsing(hashTagElement, null, null);
|
||||
this.loadElement(newElement);
|
||||
this.canGoForward = false;
|
||||
}
|
||||
|
@ -120,14 +126,14 @@ export class StreamOverlayComponent implements OnInit {
|
|||
this.currentElement = element;
|
||||
|
||||
this.accountName = this.currentElement.account;
|
||||
this.hashtag = this.currentElement.hashtag;
|
||||
this.thread = this.currentElement.thread;
|
||||
this.hashtagElement = this.currentElement.hashtag;
|
||||
}
|
||||
}
|
||||
|
||||
class OverlayBrowsing {
|
||||
constructor(
|
||||
public readonly hashtag: string,
|
||||
public readonly hashtag: StreamElement,
|
||||
public readonly account: string,
|
||||
public readonly thread: string) {
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
||||
<!-- data-simplebar -->
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="accountSelected($event)" (browseHashtag)="hashtagSelected($event)"></app-status>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
.stream-toots {
|
||||
height: calc(100%);
|
||||
width: calc(100%);
|
||||
|
||||
overflow: auto;
|
||||
&__status:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StreamStatusesComponent } from './stream-statuses.component';
|
||||
|
||||
xdescribe('StreamStatusesComponent', () => {
|
||||
let component: StreamStatusesComponent;
|
||||
let fixture: ComponentFixture<StreamStatusesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ StreamStatusesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(StreamStatusesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,208 @@
|
|||
import { Component, OnInit, Input, ViewChild, ElementRef, OnDestroy, EventEmitter, Output } from '@angular/core';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { StreamElement } from '../../../states/streams.state';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StreamingService, EventEnum, StreamingWrapper, StatusUpdate } from '../../../services/streaming.service';
|
||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { StatusWrapper } from '../stream.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-statuses',
|
||||
templateUrl: './stream-statuses.component.html',
|
||||
styleUrls: ['./stream-statuses.component.scss']
|
||||
})
|
||||
export class StreamStatusesComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _streamElement: StreamElement;
|
||||
private account: AccountInfo;
|
||||
private websocketStreaming: StreamingWrapper;
|
||||
|
||||
statuses: StatusWrapper[] = [];
|
||||
private bufferStream: Status[] = [];
|
||||
private bufferWasCleared: boolean;
|
||||
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
@Output() browseThread = new EventEmitter<string>();
|
||||
|
||||
@Input()
|
||||
set streamElement(streamElement: StreamElement) {
|
||||
console.warn('new stream');
|
||||
this.resetStream();
|
||||
|
||||
this._streamElement = streamElement;
|
||||
|
||||
const splitedUserName = streamElement.accountId.split('@');
|
||||
const user = splitedUserName[0];
|
||||
const instance = splitedUserName[1];
|
||||
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
|
||||
|
||||
this.retrieveToots();
|
||||
this.launchWebsocket();
|
||||
}
|
||||
get streamElement(): StreamElement {
|
||||
return this._streamElement;
|
||||
}
|
||||
|
||||
@Input() goToTop: Observable<void>;
|
||||
|
||||
private goToTopSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly streamingService: StreamingService,
|
||||
private readonly mastodonService: MastodonService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.goToTopSubscription = this.goToTop.subscribe(() => {
|
||||
this.applyGoToTop();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(){
|
||||
if( this.goToTopSubscription) this.goToTopSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private resetStream() {
|
||||
this.statuses.length = 0;
|
||||
this.bufferStream.length = 0;
|
||||
if(this.websocketStreaming) this.websocketStreaming.dispose();
|
||||
}
|
||||
|
||||
private launchWebsocket(): void {
|
||||
this.websocketStreaming = this.streamingService.getStreaming(this.account, this._streamElement);
|
||||
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
|
||||
if (update) {
|
||||
if (update.type === EventEnum.update) {
|
||||
if (!this.statuses.find(x => x.status.id == update.status.id)) {
|
||||
if (this.streamPositionnedAtTop) {
|
||||
const wrapper = new StatusWrapper(update.status, this.account);
|
||||
this.statuses.unshift(wrapper);
|
||||
} else {
|
||||
this.bufferStream.push(update.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkAndCleanUpStream();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
private applyGoToTop(): boolean {
|
||||
this.loadBuffer();
|
||||
if (this.statuses.length > 2 * this.streamingService.nbStatusPerIteration) {
|
||||
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
|
||||
}
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
private streamPositionnedAtTop: boolean = true;
|
||||
private isProcessingInfiniteScroll: boolean;
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000;
|
||||
const atTop = element.scrollTop === 0;
|
||||
|
||||
this.streamPositionnedAtTop = false;
|
||||
if (atBottom && !this.isProcessingInfiniteScroll) {
|
||||
this.scrolledToBottom();
|
||||
} else if (atTop) {
|
||||
this.scrolledToTop();
|
||||
}
|
||||
}
|
||||
|
||||
accountSelected(accountName: string): void {
|
||||
console.warn(`status comp: accountSelected ${accountName}`);
|
||||
this.browseAccount.next(accountName);
|
||||
}
|
||||
|
||||
hashtagSelected(hashtag: string): void {
|
||||
console.warn(`status comp: hashtagSelected ${hashtag}`);
|
||||
this.browseHashtag.next(hashtag);
|
||||
}
|
||||
|
||||
textSelected(): void {
|
||||
console.warn(`status comp: textSelected`);
|
||||
}
|
||||
|
||||
private scrolledToTop() {
|
||||
this.streamPositionnedAtTop = true;
|
||||
|
||||
this.loadBuffer();
|
||||
}
|
||||
|
||||
private loadBuffer(){
|
||||
if(this.bufferWasCleared) {
|
||||
this.statuses.length = 0;
|
||||
this.bufferWasCleared = false;
|
||||
}
|
||||
|
||||
for (const status of this.bufferStream) {
|
||||
const wrapper = new StatusWrapper(status, this.account);
|
||||
this.statuses.unshift(wrapper);
|
||||
}
|
||||
|
||||
this.bufferStream.length = 0;
|
||||
}
|
||||
|
||||
private scrolledToBottom() {
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
|
||||
const lastStatus = this.statuses[this.statuses.length - 1];
|
||||
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.list)
|
||||
.then((status: Status[]) => {
|
||||
for (const s of status) {
|
||||
const wrapper = new StatusWrapper(s, this.account);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
private getRegisteredAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts;
|
||||
}
|
||||
|
||||
|
||||
private retrieveToots(): void {
|
||||
this.mastodonService.getTimeline(this.account, this._streamElement.type, null, null, this.streamingService.nbStatusPerIteration, this._streamElement.tag, this._streamElement.list)
|
||||
.then((results: Status[]) => {
|
||||
for (const s of results) {
|
||||
const wrapper = new StatusWrapper(s, this.account);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkAndCleanUpStream(): void {
|
||||
if (this.streamPositionnedAtTop && this.statuses.length > 3 * this.streamingService.nbStatusPerIteration) {
|
||||
this.statuses.length = 2 * this.streamingService.nbStatusPerIteration;
|
||||
}
|
||||
|
||||
if (this.bufferStream.length > 3 * this.streamingService.nbStatusPerIteration) {
|
||||
this.bufferWasCleared = true;
|
||||
this.bufferStream.length = 2 * this.streamingService.nbStatusPerIteration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,10 @@
|
|||
<h1>{{ streamElement.name.toUpperCase() }}</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
||||
<!-- data-simplebar -->
|
||||
<app-stream-statuses class="stream-statuses" [streamElement]="streamElement" [goToTop]="goToTopSubject.asObservable()" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($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>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
|
@ -22,33 +22,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.stream-toots {
|
||||
.stream-statuses {
|
||||
display: block;
|
||||
height: calc(100% - 30px);
|
||||
width: 320px;
|
||||
overflow: auto;
|
||||
&__status:not(:last-child) {
|
||||
border: solid #06070b;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
// .flexcroll {
|
||||
// scrollbar-face-color: #08090d;
|
||||
// scrollbar-shadow-color: #08090d;
|
||||
// scrollbar-highlight-color: #08090d;
|
||||
// scrollbar-3dlight-color: #08090d;
|
||||
// scrollbar-darkshadow-color: #08090d;
|
||||
// scrollbar-track-color: #08090d;
|
||||
// scrollbar-arrow-color: #08090d;
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 7px;
|
||||
// }
|
||||
// &::-webkit-scrollbar-thumb {
|
||||
// -webkit-border-radius: 0px;
|
||||
// border-radius: 0px;
|
||||
// // background: #08090d;
|
||||
// background: lighten($color-primary, 5);
|
||||
// // -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
// .stream-toots {
|
||||
// height: calc(100% - 30px);
|
||||
// width: 320px;
|
||||
// overflow: auto;
|
||||
// &__status:not(:last-child) {
|
||||
// border: solid #06070b;
|
||||
// border-width: 0 0 1px 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
@ -58,48 +44,4 @@
|
|||
z-index: 50;
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
|
||||
// background-color: rgba(#ff0000, 0.3);
|
||||
// // margin: 0 0 0 $stream-column-separator;
|
||||
// // outline: 1px red solid;
|
||||
// // float: left;
|
||||
// &__header {
|
||||
// width: calc(100%);
|
||||
// height: 30px;
|
||||
// background-color: $column-header-background-color;
|
||||
// padding: 6px 10px 0 10px;
|
||||
// & a {
|
||||
// color: whitesmoke;
|
||||
// font-size: 0.8em;
|
||||
// font-weight: normal;
|
||||
// margin: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &__title {
|
||||
// width: calc(100%);
|
||||
// height: 30px;
|
||||
// background-color: $column-header-background-color;
|
||||
// border-top: 1px solid whitesmoke;
|
||||
// border-bottom: 1px solid whitesmoke;
|
||||
// padding: 3px 10px 0 10px;
|
||||
// }
|
||||
}
|
||||
|
||||
// .overlay-previous {
|
||||
// display: block;
|
||||
// float: left;
|
||||
// }
|
||||
|
||||
|
||||
// .overlay-next {
|
||||
// display: block;
|
||||
// float: right;
|
||||
// padding-right: 20px;
|
||||
// }
|
||||
|
||||
|
||||
// .overlay-close {
|
||||
// display: block;
|
||||
// float: right;
|
||||
// }
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import { Component, OnInit, Input, ElementRef, ViewChild, HostListener } from "@angular/core";
|
||||
import { AccountWrapper } from "../../models/account.models";
|
||||
import { StreamElement, StreamTypeEnum } from "../../states/streams.state";
|
||||
import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service";
|
||||
import { Store } from "@ngxs/store";
|
||||
import { AccountInfo } from "../../states/accounts.state";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { StreamElement } from "../../states/streams.state";
|
||||
import { Status } from "../../services/models/mastodon.interfaces";
|
||||
import { MastodonService } from "../../services/mastodon.service";
|
||||
import { AccountInfo } from "../../states/accounts.state";
|
||||
|
||||
@Component({
|
||||
selector: "app-stream",
|
||||
|
@ -13,55 +11,34 @@ import { MastodonService } from "../../services/mastodon.service";
|
|||
styleUrls: ["./stream.component.scss"]
|
||||
})
|
||||
export class StreamComponent implements OnInit {
|
||||
private _streamElement: StreamElement;
|
||||
private account: AccountInfo;
|
||||
private websocketStreaming: StreamingWrapper;
|
||||
|
||||
statuses: StatusWrapper[] = [];
|
||||
private bufferStream: Status[] = [];
|
||||
private bufferWasCleared: boolean;
|
||||
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: string;
|
||||
overlayHashtagToBrowse: string;
|
||||
|
||||
@Input()
|
||||
set streamElement(streamElement: StreamElement) {
|
||||
this._streamElement = streamElement;
|
||||
goToTopSubject: Subject<void> = new Subject<void>();
|
||||
|
||||
const splitedUserName = streamElement.accountId.split('@');
|
||||
const user = splitedUserName[0];
|
||||
const instance = splitedUserName[1];
|
||||
this.account = this.getRegisteredAccounts().find(x => x.username == user && x.instance == instance);
|
||||
@Input() streamElement: StreamElement;
|
||||
|
||||
this.retrieveToots();
|
||||
this.launchWebsocket();
|
||||
}
|
||||
|
||||
get streamElement(): StreamElement {
|
||||
return this._streamElement;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly streamingService: StreamingService,
|
||||
private readonly mastodonService: MastodonService) {
|
||||
}
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
goToTop(): boolean {
|
||||
this.goToTopSubject.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
browseAccount(account: string): void {
|
||||
this.overlayAccountToBrowse = account;
|
||||
this.overlayHashtagToBrowse = null;
|
||||
this.overlayActive = true;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: string): void {
|
||||
console.warn(`browseHashtag ${hashtag}`);
|
||||
this.overlayAccountToBrowse = null;
|
||||
this.overlayHashtagToBrowse = hashtag;
|
||||
this.overlayActive = true;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseThread(thread: string): void {
|
||||
|
@ -73,127 +50,11 @@ export class StreamComponent implements OnInit {
|
|||
this.overlayAccountToBrowse = null;
|
||||
this.overlayActive = false;
|
||||
}
|
||||
|
||||
@ViewChild('statusstream') public statustream: ElementRef;
|
||||
goToTop(): boolean {
|
||||
this.loadBuffer();
|
||||
if (this.statuses.length > 40) {
|
||||
this.statuses.length = 40;
|
||||
}
|
||||
const stream = this.statustream.nativeElement as HTMLElement;
|
||||
stream.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
private streamPositionnedAtTop: boolean = true;
|
||||
private isProcessingInfiniteScroll: boolean;
|
||||
|
||||
onScroll() {
|
||||
var element = this.statustream.nativeElement as HTMLElement;
|
||||
const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 500;
|
||||
const atTop = element.scrollTop === 0;
|
||||
|
||||
this.streamPositionnedAtTop = false;
|
||||
if (atBottom && !this.isProcessingInfiniteScroll) {
|
||||
this.scrolledToBottom();
|
||||
} else if (atTop) {
|
||||
this.scrolledToTop();
|
||||
}
|
||||
}
|
||||
|
||||
private scrolledToTop() {
|
||||
this.streamPositionnedAtTop = true;
|
||||
|
||||
this.loadBuffer();
|
||||
}
|
||||
|
||||
private loadBuffer(){
|
||||
if(this.bufferWasCleared) {
|
||||
this.statuses.length = 0;
|
||||
this.bufferWasCleared = false;
|
||||
}
|
||||
|
||||
for (const status of this.bufferStream) {
|
||||
const wrapper = new StatusWrapper(status, this.account);
|
||||
this.statuses.unshift(wrapper);
|
||||
}
|
||||
|
||||
this.bufferStream.length = 0;
|
||||
}
|
||||
|
||||
private scrolledToBottom() {
|
||||
this.isProcessingInfiniteScroll = true;
|
||||
|
||||
const lastStatus = this.statuses[this.statuses.length - 1];
|
||||
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id)
|
||||
.then((status: Status[]) => {
|
||||
for (const s of status) {
|
||||
const wrapper = new StatusWrapper(s, this.account);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
this.isProcessingInfiniteScroll = false;
|
||||
});
|
||||
}
|
||||
|
||||
private getRegisteredAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts;
|
||||
}
|
||||
|
||||
private retrieveToots(): void {
|
||||
this.mastodonService.getTimeline(this.account, this._streamElement.type)
|
||||
.then((results: Status[]) => {
|
||||
for (const s of results) {
|
||||
const wrapper = new StatusWrapper(s, this.account);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private launchWebsocket(): void {
|
||||
this.websocketStreaming = this.streamingService.getStreaming(this.account, this._streamElement.type);
|
||||
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
|
||||
if (update) {
|
||||
if (update.type === EventEnum.update) {
|
||||
if (!this.statuses.find(x => x.status.id == update.status.id)) {
|
||||
if (this.streamPositionnedAtTop) {
|
||||
const wrapper = new StatusWrapper(update.status, this.account);
|
||||
this.statuses.unshift(wrapper);
|
||||
} else {
|
||||
this.bufferStream.push(update.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkAndCleanUpStream();
|
||||
});
|
||||
}
|
||||
|
||||
private checkAndCleanUpStream(): void {
|
||||
if (this.streamPositionnedAtTop && this.statuses.length > 60) {
|
||||
this.statuses.length = 40;
|
||||
}
|
||||
|
||||
if (this.bufferStream.length > 60) {
|
||||
this.bufferWasCleared = true;
|
||||
this.bufferStream.length = 40;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusWrapper {
|
||||
constructor(
|
||||
public status: Status,
|
||||
public provider: AccountInfo
|
||||
) {}
|
||||
) { }
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<div class="profile flexcroll">
|
||||
<div class="profile">
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
||||
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
|
||||
|
@ -9,20 +9,23 @@
|
|||
<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)="accountSelected($event)"
|
||||
(hashtagSelected)="hashtagSelected($event)"></app-databinded-text>
|
||||
<!-- <p innerHTML="{{account.note}}"></p> -->
|
||||
</div>
|
||||
<div class="profile-statuses">
|
||||
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
||||
<div *ngIf="!isLoading && !statusLoading && statuses.length == 0" class="profile-no-toots">
|
||||
no toots found
|
||||
<div class="profile-sub-header flexcroll">
|
||||
<div *ngIf="account && hasNote" class="profile-description">
|
||||
<app-databinded-text class="status__content" [textIsSelectable]="false" [text]="account.note"
|
||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"></app-databinded-text>
|
||||
<!-- <p innerHTML="{{account.note}}"></p> -->
|
||||
</div>
|
||||
<div class="profile-statuses">
|
||||
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
||||
<div *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="accountSelected($event)"></app-status>
|
||||
<div *ngIf="!isLoading && !statusLoading && statuses.length == 0" class="profile-no-toots">
|
||||
no toots found
|
||||
</div>
|
||||
|
||||
<div *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="accountSelected($event)"></app-status>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,10 @@
|
|||
@import "variables";
|
||||
@import "commons";
|
||||
|
||||
$header-height: 160px;
|
||||
|
||||
.profile {
|
||||
overflow: auto;
|
||||
// overflow: auto;
|
||||
height: calc(100% - 30px);
|
||||
|
||||
&-header {
|
||||
|
@ -14,7 +17,7 @@
|
|||
}
|
||||
&__inner {
|
||||
overflow: auto;
|
||||
height: 160px;
|
||||
height: $header-height;
|
||||
background-color: rgba(0, 0, 0, .45);
|
||||
}
|
||||
&__avatar {
|
||||
|
@ -38,6 +41,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-sub-header {
|
||||
overflow: auto;
|
||||
height: calc(100% - #{$header-height});
|
||||
// height: calc(20% - 190px);
|
||||
// height: 150px;
|
||||
// border: 1px solid greenyellow;
|
||||
}
|
||||
|
||||
&-description {
|
||||
padding: 10px 10px 15px 10px;
|
||||
font-size: 13px;
|
||||
|
|
|
@ -33,20 +33,15 @@ export class UserProfileComponent implements OnInit {
|
|||
this.loadAccount(accountName)
|
||||
.then((account: Account) => {
|
||||
this.account = account;
|
||||
this.hasNote = account && account.note && account.note !== '<p></p>';
|
||||
return this.getStatuses(this.account);
|
||||
})
|
||||
.catch(err => {
|
||||
this.error = 'Error when retrieving account';
|
||||
this.isLoading = false;
|
||||
this.statusLoading = false;
|
||||
console.warn(this.error);
|
||||
console.error(this.error);
|
||||
});
|
||||
|
||||
// this.account = account;
|
||||
// this.hasNote = account && account.note && account.note !== '<p></p>';
|
||||
// console.warn('currentAccount');
|
||||
// console.warn(account);
|
||||
// this.getStatuses(account);
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
@ -76,11 +71,10 @@ export class UserProfileComponent implements OnInit {
|
|||
}
|
||||
|
||||
this.isLoading = true;
|
||||
return this.mastodonService.search(selectedAccounts[0], accountName, true)
|
||||
.then((result: Results) => {
|
||||
console.warn(result);
|
||||
return this.toolsService.findAccount(selectedAccounts[0], accountName)
|
||||
.then((result) => {
|
||||
this.isLoading = false;
|
||||
return result.accounts[0];
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TimeAgoPipe } from './time-ago.pipe';
|
||||
|
||||
describe('TimeAgoPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new TimeAgoPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
xdescribe('TimeAgoPipe', () => {
|
||||
// it('create an instance', () => {
|
||||
// const pipe = new TimeAgoPipe();
|
||||
// expect(pipe).toBeTruthy();
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -19,13 +19,13 @@ export class MastodonService {
|
|||
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20): Promise<Status[]> {
|
||||
const route = `https://${account.instance}${this.getTimelineRoute(type, max_id, since_id, limit)}`;
|
||||
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20, tag: string = null, list: string = null): Promise<Status[]> {
|
||||
const route = `https://${account.instance}${this.getTimelineRoute(type, max_id, since_id, limit, tag, list)}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
private getTimelineRoute(type: StreamTypeEnum, max_id: string, since_id: string, limit: number): string {
|
||||
private getTimelineRoute(type: StreamTypeEnum, max_id: string, since_id: string, limit: number, tag: string, list: string): string {
|
||||
let route: string;
|
||||
switch (type) {
|
||||
case StreamTypeEnum.personnal:
|
||||
|
@ -41,10 +41,10 @@ export class MastodonService {
|
|||
route = this.apiRoutes.getDirectTimeline;
|
||||
break;
|
||||
case StreamTypeEnum.tag:
|
||||
route = this.apiRoutes.getTagTimeline.replace('{0}', 'TODO');
|
||||
route = this.apiRoutes.getTagTimeline.replace('{0}', tag);
|
||||
break;
|
||||
case StreamTypeEnum.list:
|
||||
route = this.apiRoutes.getListTimeline.replace('{0}', 'TODO');
|
||||
route = this.apiRoutes.getListTimeline.replace('{0}', list);
|
||||
break;
|
||||
default:
|
||||
throw new Error('StreamTypeEnum not supported');
|
||||
|
@ -112,6 +112,7 @@ export class MastodonService {
|
|||
}
|
||||
|
||||
search(account: AccountInfo, query: string, resolve: boolean = false): Promise<Results>{
|
||||
if(query[0] === '#') query = query.substr(1);
|
||||
const route = `https://${account.instance}${this.apiRoutes.search}?q=${query}&resolve=${resolve}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Results>(route, { headers: headers }).toPromise()
|
||||
|
|
|
@ -2,18 +2,21 @@ import { Injectable } from "@angular/core";
|
|||
import { Status } from "./models/mastodon.interfaces";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { ApiRoutes } from "./models/api.settings";
|
||||
import { StreamTypeEnum } from "../states/streams.state";
|
||||
import { StreamTypeEnum, StreamElement } from "../states/streams.state";
|
||||
import { MastodonService } from "./mastodon.service";
|
||||
import { AccountInfo } from "../states/accounts.state";
|
||||
import { stat } from "fs";
|
||||
|
||||
@Injectable()
|
||||
export class StreamingService {
|
||||
|
||||
public readonly nbStatusPerIteration: number = 20;
|
||||
|
||||
constructor(
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
getStreaming(accountInfo: AccountInfo, streamType: StreamTypeEnum): StreamingWrapper {
|
||||
return new StreamingWrapper(this.mastodonService, accountInfo, streamType);
|
||||
getStreaming(accountInfo: AccountInfo, stream: StreamElement): StreamingWrapper {
|
||||
return new StreamingWrapper(this.mastodonService, accountInfo, stream, this.nbStatusPerIteration);
|
||||
}
|
||||
|
||||
|
||||
|
@ -23,84 +26,68 @@ export class StreamingWrapper {
|
|||
statusUpdateSubjet = new BehaviorSubject<StatusUpdate>(null);
|
||||
eventSource: WebSocket;
|
||||
private apiRoutes = new ApiRoutes();
|
||||
private errorClosing: boolean;
|
||||
private since_id: string;
|
||||
private disposed: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly mastodonService: MastodonService,
|
||||
private readonly accountInfo: AccountInfo,
|
||||
private readonly streamType: StreamTypeEnum) {
|
||||
private readonly account: AccountInfo,
|
||||
private readonly stream: StreamElement,
|
||||
private readonly nbStatusPerIteration: number) {
|
||||
|
||||
const request = this.getRequest(streamType);
|
||||
const route = `wss://${accountInfo.instance}${this.apiRoutes.getStreaming}`.replace('{0}', accountInfo.token.access_token).replace('{1}', request);
|
||||
const route = this.getRoute(account, stream);
|
||||
this.start(route);
|
||||
}
|
||||
|
||||
dispose(): any {
|
||||
this.disposed = true;
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
private start(route: string) {
|
||||
this.eventSource = new WebSocket(route);
|
||||
this.eventSource.onmessage = x => this.statusParsing(<WebSocketEvent>JSON.parse(x.data));
|
||||
this.eventSource.onerror = x => this.webSocketGotError(x);
|
||||
this.eventSource.onopen = x => console.log(x);
|
||||
this.eventSource.onclose = x => this.webSocketClosed(route, x);
|
||||
this.eventSource.onclose = x => this.webSocketClosed(route, x);
|
||||
}
|
||||
|
||||
private errorClosing: boolean;
|
||||
private webSocketGotError(x: Event) {
|
||||
console.log(x);
|
||||
this.errorClosing = true;
|
||||
}
|
||||
|
||||
private since_id: string;
|
||||
private webSocketClosed(domain, x: Event) {
|
||||
console.log(x);
|
||||
|
||||
if (this.errorClosing) {
|
||||
|
||||
this.pullNewStatuses(domain);
|
||||
|
||||
// this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
|
||||
// .then((status: Status[]) => {
|
||||
// // status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
|
||||
// status = status.sort((a, b) => a.id.localeCompare(b.id));
|
||||
// for (const s of status) {
|
||||
// const update = new StatusUpdate();
|
||||
// update.status = s;
|
||||
// update.type = EventEnum.update;
|
||||
// this.since_id = update.status.id;
|
||||
// this.statusUpdateSubjet.next(update);
|
||||
// }
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.error(err);
|
||||
// })
|
||||
// .then(() => {
|
||||
// setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
// });
|
||||
|
||||
this.errorClosing = false;
|
||||
} else {
|
||||
} else if (!this.disposed) {
|
||||
setTimeout(() => { this.start(domain) }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private pullNewStatuses(domain){
|
||||
this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
|
||||
.then((status: Status[]) => {
|
||||
// status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
|
||||
status = status.sort((a, b) => a.id.localeCompare(b.id));
|
||||
for (const s of status) {
|
||||
const update = new StatusUpdate();
|
||||
update.status = s;
|
||||
update.type = EventEnum.update;
|
||||
this.since_id = update.status.id;
|
||||
this.statusUpdateSubjet.next(update);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
// setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
private pullNewStatuses(domain) {
|
||||
this.mastodonService.getTimeline(this.account, this.stream.type, null, this.since_id, this.nbStatusPerIteration, this.stream.tag, this.stream.list)
|
||||
.then((status: Status[]) => {
|
||||
// status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
|
||||
status = status.sort((a, b) => a.id.localeCompare(b.id));
|
||||
for (const s of status) {
|
||||
const update = new StatusUpdate();
|
||||
update.status = s;
|
||||
update.type = EventEnum.update;
|
||||
this.since_id = update.status.id;
|
||||
this.statusUpdateSubjet.next(update);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
// setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
if (!this.disposed) {
|
||||
setTimeout(() => { this.pullNewStatuses(domain) }, 15 * 1000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private statusParsing(event: WebSocketEvent) {
|
||||
|
@ -122,7 +109,17 @@ export class StreamingWrapper {
|
|||
this.statusUpdateSubjet.next(newUpdate);
|
||||
}
|
||||
|
||||
private getRequest(type: StreamTypeEnum): string {
|
||||
private getRoute(account: AccountInfo, stream: StreamElement): string {
|
||||
const streamingRouteType = this.getStreamingRouteType(stream.type);
|
||||
let route = `wss://${account.instance}${this.apiRoutes.getStreaming}`.replace('{0}', account.token.access_token).replace('{1}', streamingRouteType);
|
||||
|
||||
if (stream.tag) route = `${route}&tag=${stream.tag}`;
|
||||
if (stream.list) route = `${route}&tag=${stream.list}`;
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
private getStreamingRouteType(type: StreamTypeEnum): string {
|
||||
switch (type) {
|
||||
case StreamTypeEnum.global:
|
||||
return 'public';
|
||||
|
@ -130,6 +127,14 @@ export class StreamingWrapper {
|
|||
return 'public:local';
|
||||
case StreamTypeEnum.personnal:
|
||||
return 'user';
|
||||
case StreamTypeEnum.directmessages:
|
||||
return 'direct';
|
||||
case StreamTypeEnum.tag:
|
||||
return 'hashtag';
|
||||
case StreamTypeEnum.list:
|
||||
return 'list';
|
||||
default:
|
||||
throw Error('Not supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
|||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
import { MastodonService } from './mastodon.service';
|
||||
import { Account, Results } from "./models/mastodon.interfaces";
|
||||
|
||||
|
||||
@Injectable({
|
||||
|
@ -9,7 +11,9 @@ import { AccountInfo } from '../states/accounts.state';
|
|||
})
|
||||
export class ToolsService {
|
||||
|
||||
constructor( private readonly store: Store) { }
|
||||
constructor(
|
||||
private readonly mastodonService: MastodonService,
|
||||
private readonly store: Store) { }
|
||||
|
||||
|
||||
getSelectedAccounts(): AccountInfo[] {
|
||||
|
@ -17,4 +21,21 @@ export class ToolsService {
|
|||
return regAccounts.filter(x => x.isSelected);
|
||||
}
|
||||
|
||||
findAccount(account: AccountInfo, accountName: string): Promise<Account> {
|
||||
return this.mastodonService.search(account, accountName, true)
|
||||
.then((result: Results) => {
|
||||
console.warn('findAccount');
|
||||
console.warn(`accountName ${accountName}`);
|
||||
console.warn(result);
|
||||
|
||||
if(accountName[0] === '@') accountName = accountName.substr(1);
|
||||
|
||||
const foundAccount = result.accounts.filter(
|
||||
x => x.acct.toLowerCase() === accountName.toLowerCase()
|
||||
|| x.acct.toLowerCase() === accountName.toLowerCase().split('@')[0]
|
||||
)[0];
|
||||
return foundAccount;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,8 +26,12 @@ export class StreamsState {
|
|||
}
|
||||
|
||||
export class StreamElement {
|
||||
constructor(public type: StreamTypeEnum, public name: string, public accountId: string) {
|
||||
|
||||
constructor(
|
||||
public type: StreamTypeEnum,
|
||||
public name: string,
|
||||
public accountId: string,
|
||||
public tag: string,
|
||||
public list: string) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,4 +30,17 @@
|
|||
// &:visited {
|
||||
// outline: none;
|
||||
// }
|
||||
}
|
||||
|
||||
.btn-custom-secondary {
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background-color: $button-background-color;
|
||||
color: $button-color;
|
||||
transition: all .2s;
|
||||
&:hover {
|
||||
background-color: $button-background-color-hover;
|
||||
color: $button-color-hover;
|
||||
}
|
||||
}
|
|
@ -33,4 +33,11 @@ $stream-column-separator: 7px;
|
|||
$stream-column-width: 320px;
|
||||
$avatar-column-space: 70px;
|
||||
//Bootstrap cuistomization
|
||||
$enable-rounded: false;
|
||||
$enable-rounded: false;
|
||||
|
||||
$separator-color:$color-primary;
|
||||
$button-size: 30px;
|
||||
$button-color: darken(white, 30);
|
||||
$button-color-hover: white;
|
||||
$button-background-color: $color-primary;
|
||||
$button-background-color-hover: lighten($color-primary, 20);
|
Loading…
Reference in New Issue