Merge pull request #16 from NicolasConstant/feature_open-user-profile

Feature open user profile
This commit is contained in:
Nicolas Constant 2018-10-28 15:22:45 -04:00 committed by GitHub
commit 3b41795a83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1400 additions and 132 deletions

View File

@ -32,6 +32,14 @@ import { AddNewAccountComponent } from './components/floating-column/add-new-acc
import { SearchComponent } from './components/floating-column/search/search.component';
import { AddNewStatusComponent } from "./components/floating-column/add-new-status/add-new-status.component";
import { ManageAccountComponent } from "./components/floating-column/manage-account/manage-account.component";
import { ActionBarComponent } from './components/stream/status/action-bar/action-bar.component';
import { WaitingAnimationComponent } from './components/waiting-animation/waiting-animation.component';
import { ReplyToStatusComponent } from './components/stream/status/reply-to-status/reply-to-status.component';
import { UserProfileComponent } from './components/stream/user-profile/user-profile.component';
import { ThreadComponent } from './components/stream/thread/thread.component';
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';
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
@ -56,7 +64,15 @@ const routes: Routes = [
AttachementsComponent,
SettingsComponent,
AddNewAccountComponent,
SearchComponent
SearchComponent,
ActionBarComponent,
WaitingAnimationComponent,
ReplyToStatusComponent,
UserProfileComponent,
ThreadComponent,
HashtagComponent,
StreamOverlayComponent,
DatabindedTextComponent
],
imports: [
BrowserModule,

View File

@ -3,10 +3,9 @@
<form (ngSubmit)="onSubmit()">
<!-- <label>Please provide your account:</label> -->
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off"
placeholder="Title (optional)" />
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off" placeholder="Title (optional)" />
<!-- <textarea rows="4" cols="50"> -->
<textarea [(ngModel)]="status" name="status" class="form-control form-control-sm" style="min-width: 100%" rows="5" required placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm" style="min-width: 100%" rows="5" required placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
<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>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, ElementRef, ViewChild } from '@angular/core';
import { Store } from '@ngxs/store';
import { AccountInfo } from '../../../states/accounts.state';
import { MastodonService, VisibilityEnum } from '../../../services/mastodon.service';
@ -13,14 +13,19 @@ import { FormsModule } from '@angular/forms';
export class AddNewStatusComponent implements OnInit {
@Input() title: string;
@Input() status: string;
@ViewChild('reply') replyElement: ElementRef;
selectedPrivacy = 'Public';
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
constructor(private readonly store: Store,
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService) { }
ngOnInit() {
setTimeout(() => {
this.replyElement.nativeElement.focus();
}, 0);
}
onSubmit(): boolean {

View File

@ -3,27 +3,29 @@
<form (ngSubmit)="onSubmit()">
<input type="text" class="form-control form-control-sm" [(ngModel)]="searchHandle" name="searchHandle"
placeholder="Search" autocomplete="off"/>
placeholder="Search" autocomplete="off" />
</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">
<img src="{{account.avatar}}" class="account__avatar" />
<div class="account__name">{{ account.username }}</div>
<div class="account__fullhandle">@{{ account.acct }}</div>
</a>
</div>
</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">
<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">
#{{ hashtag }}
</a>
</a>
</div>
</div>

View File

@ -1,6 +1,7 @@
@import "variables";
@import "mixins";
@import "panel";
@import "commons";
$separator-color:$color-primary;

View File

@ -18,6 +18,8 @@ export class SearchComponent implements OnInit {
statuses: Status[] = [];
hashtags: string[] = [];
isLoading: boolean;
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService) { }
@ -35,6 +37,7 @@ export class SearchComponent implements OnInit {
this.accounts.length = 0;
this.statuses.length = 0;
this.hashtags.length = 0;
this.isLoading = true;
console.warn(`search: ${data}`);
@ -57,7 +60,8 @@ export class SearchComponent implements OnInit {
}
}
})
.catch((err) => console.error(err));
.catch((err) => console.error(err))
.then(() => { this.isLoading = false; });
}
}

View File

@ -0,0 +1,3 @@
<p>
hashtag works!
</p>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HashtagComponent } from './hashtag.component';
describe('HashtagComponent', () => {
let component: HashtagComponent;
let fixture: ComponentFixture<HashtagComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HashtagComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HashtagComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-hashtag',
templateUrl: './hashtag.component.html',
styleUrls: ['./hashtag.component.scss']
})
export class HashtagComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,21 @@
<div class="action-bar">
<a *ngIf="!isLocked" href class="action-bar__link" title="Reply" (click)="reply()">
<ion-icon name="ios-undo"></ion-icon>
</a>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon>
<a *ngIf="!(isBoostLocked || isLocked)" href class="action-bar__link" title="Boost" [class.boosted]="isBoosted" (click)="boost()">
<ion-icon name="md-swap"></ion-icon>
</a>
<ion-icon *ngIf="isBoostLocked && !isLocked" class="action-bar__lock" name="lock" title="This post cannot be boosted"></ion-icon>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon>
<a *ngIf="!isLocked" href class="action-bar__link" title="Favourite" [class.favorited]="isFavorited" (click)="favorite()">
<ion-icon name="md-star"></ion-icon>
</a>
<ion-icon *ngIf="isLocked" class="action-bar__lock" name="lock" title="Account can't access this post"></ion-icon>
<a href class="action-bar__link" title="More" (click)="more()">
<ion-icon name="ios-more"></ion-icon>
</a>
</div>

View File

@ -0,0 +1,42 @@
@import "variables";
.action-bar {
// outline: 1px solid greenyellow; // height: 20px;
margin: 5px 10px 5px $avatar-column-space;
padding: 0;
font-size: 24px;
height: 30px;
&__link {
color: $status-secondary-color;
&:hover {
color: $status-links-color;
}
&:not(:last-child) {
margin-right: 15px;
}
}
&__lock {
color: $status-secondary-color;
width: 24px;
&:not(:last-child) {
margin-right: 15px;
}
}
}
.boosted {
color: $boost-color;
&:hover {
color: darken($boost-color, 10);
}
}
.favorited {
color: $favorite-color;
&:hover {
color: darken($favorite-color, 10);
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActionBarComponent } from './action-bar.component';
describe('ActionBarComponent', () => {
let component: ActionBarComponent;
let fixture: ComponentFixture<ActionBarComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ActionBarComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ActionBarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,191 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core';
import { Store } from '@ngxs/store';
import { Observable, Subscription } from 'rxjs';
import { StatusWrapper } from '../../stream.component';
import { MastodonService } from '../../../../services/mastodon.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { Status, Results } from '../../../../services/models/mastodon.interfaces';
// import { map } from "rxjs/operators";
@Component({
selector: 'app-action-bar',
templateUrl: './action-bar.component.html',
styleUrls: ['./action-bar.component.scss']
})
export class ActionBarComponent implements OnInit, OnDestroy {
@Input() statusWrapper: StatusWrapper;
@Output() replyEvent = new EventEmitter();
isFavorited: boolean;
isBoosted: boolean;
isBoostLocked: boolean;
isLocked: boolean;
private isProviderSelected: boolean;
private selectedAccounts: AccountInfo[];
private favoriteStatePerAccountId: { [id: string]: boolean; } = {};
private bootedStatePerAccountId: { [id: string]: boolean; } = {};
private accounts$: Observable<AccountInfo[]>;
private accountSub: Subscription;
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
ngOnInit() {
// const selectedAccounts = this.getSelectedAccounts();
// this.checkStatus(selectedAccounts);
const status = this.statusWrapper.status;
const account = this.statusWrapper.provider;
this.favoriteStatePerAccountId[account.id] = status.favourited;
this.bootedStatePerAccountId[account.id] = status.reblogged;
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
this.checkStatus(accounts);
});
}
ngOnDestroy(): void {
this.accountSub.unsubscribe();
}
private checkStatus(accounts: AccountInfo[]): void {
const status = this.statusWrapper.status;
const provider = this.statusWrapper.provider;
this.selectedAccounts = accounts.filter(x => x.isSelected);
this.isProviderSelected = this.selectedAccounts.filter(x => x.id === provider.id).length > 0;
if (status.visibility === 'direct' || status.visibility === 'private') {
this.isBoostLocked = true;
} else {
this.isBoostLocked = false;
}
if ((status.visibility === 'direct' || status.visibility === 'private') && !this.isProviderSelected) {
this.isLocked = true;
} else {
this.isLocked = false;
}
this.checkIfFavorited();
this.checkIfBoosted();
}
reply(): boolean {
this.replyEvent.emit();
return false;
}
boost(): boolean {
this.selectedAccounts.forEach((account: AccountInfo) => {
const isProvider = this.statusWrapper.provider.id === account.id;
let pipeline: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (!isProvider) {
pipeline = pipeline.then((foreignStatus: Status) => {
const statusUrl = foreignStatus.url;
return this.mastodonService.search(account, statusUrl)
.then((results: Results) => {
//TODO check and type errors
return results.statuses[0];
});
});
}
pipeline
.then((status: Status) => {
if (this.isBoosted) {
return this.mastodonService.unreblog(account, status);
} else {
return this.mastodonService.reblog(account, status);
}
})
.then((boostedStatus: Status) => {
this.bootedStatePerAccountId[account.id] = boostedStatus.reblogged;
this.checkIfBoosted();
// this.isBoosted = !this.isBoosted;
})
.catch(err => {
console.error(err);
});
});
return false;
}
favorite(): boolean {
this.selectedAccounts.forEach((account: AccountInfo) => {
const isProvider = this.statusWrapper.provider.id === account.id;
let pipeline: Promise<Status> = Promise.resolve(this.statusWrapper.status);
if (!isProvider) {
pipeline = pipeline.then((foreignStatus: Status) => {
const statusUrl = foreignStatus.url;
return this.mastodonService.search(account, statusUrl)
.then((results: Results) => {
//TODO check and type errors
return results.statuses[0];
});
});
}
pipeline
.then((status: Status) => {
if (this.isFavorited) {
return this.mastodonService.unfavorite(account, status);
} else {
return this.mastodonService.favorite(account, status);
}
})
.then((favoritedStatus: Status) => {
this.favoriteStatePerAccountId[account.id] = favoritedStatus.favourited;
this.checkIfFavorited();
// this.isFavorited = !this.isFavorited;
})
.catch(err => {
console.error(err);
});
});
return false;
}
private checkIfBoosted() {
const selectedAccount = <AccountInfo>this.selectedAccounts[0];
if (selectedAccount) {
this.isBoosted = this.bootedStatePerAccountId[selectedAccount.id];
} else {
this.isBoosted = false;
}
}
private checkIfFavorited() {
const selectedAccount = <AccountInfo>this.selectedAccounts[0];
if (selectedAccount) {
this.isFavorited = this.favoriteStatePerAccountId[selectedAccount.id];
} else {
this.isFavorited = false;
}
}
more(): boolean {
console.warn('more');
return false;
}
private getSelectedAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
}

View File

@ -0,0 +1 @@
<div #content class="content" innerHTML="{{processedText}}" (click)="selectText()"></div>

View File

@ -0,0 +1,24 @@
@import "variables";
.content {
cursor: pointer;
}
//Mastodon styling
:host ::ng-deep .content {
// 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;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DatabindedTextComponent } from './databinded-text.component';
describe('DatabindedTextComponent', () => {
let component: DatabindedTextComponent;
let fixture: ComponentFixture<DatabindedTextComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DatabindedTextComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DatabindedTextComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,125 @@
import { Component, OnInit, Input, EventEmitter, Output, Renderer2, ViewChild, ElementRef } from '@angular/core';
import { forEach } from '@angular/router/src/utils/collection';
@Component({
selector: 'app-databinded-text',
templateUrl: './databinded-text.component.html',
styleUrls: ['./databinded-text.component.scss']
})
export class DatabindedTextComponent implements OnInit {
private accounts: string[] = [];
private hashtags: string[] = [];
// private links: string[] = [];
processedText: string;
@ViewChild('content') contentElement: ElementRef;
@Output() accountSelected = new EventEmitter<string>();
@Output() hashtagSelected = new EventEmitter<string>();
@Output() textSelected = new EventEmitter();
@Input('text')
set text(value: string) {
this.processedText = '';
let linksSections = value.split('<a ');
for (let section of linksSections) {
if (!section.includes('href')) {
this.processedText += section;
continue;
}
if (section.includes('class="mention hashtag"')) {
let extractedLinkAndNext = section.split('</a>');
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span>', '').replace('</span>', '');
this.processedText += ` <a href class="${extractedHashtag}">#${extractedHashtag}</a>`;
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
this.hashtags.push(extractedHashtag);
} else if (section.includes('class="u-url mention"')) {
let extractedAccountAndNext = section.split('</a></span>');
let extractedAccountName = extractedAccountAndNext[0].split('@<span>')[1].replace('<span>', '').replace('</span>', '');
let extractedAccountLink = extractedAccountAndNext[0].split('" class="u-url mention"')[0].replace('href="https://', '').replace(' ', '').replace('@', '').split('/');
let extractedAccount = `@${extractedAccountLink[1]}@${extractedAccountLink[0]}`;
let classname = this.getClassName(extractedAccount);
this.processedText += ` <a href class="${classname}" title="${extractedAccount}">@${extractedAccountName}</a>`;
if (extractedAccountAndNext[1]) this.processedText += extractedAccountAndNext[1];
this.accounts.push(extractedAccount);
} else {
this.processedText += `<a class="link" ${section}`;
}
}
}
constructor(private renderer: Renderer2) { }
ngOnInit() {
}
ngAfterViewInit() {
for (const hashtag of this.hashtags) {
let el = this.contentElement.nativeElement.querySelector(`.${hashtag}`);
this.renderer.listen(el, 'click', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.selectHashtag(hashtag);
return false;
});
}
for (const account of this.accounts) {
let classname = this.getClassName(account);
let el = this.contentElement.nativeElement.querySelector(`.${classname}`);
this.renderer.listen(el, 'click', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
this.selectAccount(account);
return false;
});
}
// let allLinksEl = this.contentElement.nativeElement.querySelectorAll(`.link`);
// for (const link of allLinksEl) {
// this.renderer.listen(link, 'click', (event) => {
// //event.preventDefault();
// event.stopImmediatePropagation();
// return false;
// });
// }
}
private getClassName(value: string): string {
let res = value;
while(res.includes('.')) res = res.replace('.', '-');
while(res.includes('@')) res = res.replace('@', '-');
return res;
}
private selectAccount(account: string) {
console.warn(`select ${account}`);
this.accountSelected.next(account);
}
private selectHashtag(hashtag: string) {
console.warn(`select ${hashtag}`);
this.hashtagSelected.next(hashtag);
}
selectText() {
console.warn(`selectText`);
this.textSelected.next();
}
}

View File

@ -0,0 +1,8 @@
<form (ngSubmit)="onSubmit()">
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm" rows="5" required placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
<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">REPLY!</button>
</form>

View File

@ -0,0 +1,36 @@
@import "variables";
@import "panel";
@import "buttons";
$btn-send-status-width: 60px;
.form-control {
margin: 0 0 5px 5px;
width: calc(100% - 10px);
background-color: $column-color;
border-color: $status-secondary-color;
color: #fff;
font-size: $default-font-size;
&:focus {
box-shadow: none;
}
&--privacy {
display: inline-block;
width: calc(100% - 15px - #{$btn-send-status-width});
}
}
.btn-custom-primary {
display: inline-block;
width: $btn-send-status-width;
position: relative;
top: -1px;
left: 5px; // background-color: orange;
// border-color: orange;
// color: black;
font-weight: 500; // &:hover {
// }
// &:focus {
// border-color: darkblue;
// }
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ReplyToStatusComponent } from './reply-to-status.component';
describe('ReplyToStatusComponent', () => {
let component: ReplyToStatusComponent;
let fixture: ComponentFixture<ReplyToStatusComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ReplyToStatusComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ReplyToStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import { Component, OnInit, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
import { Store } from '@ngxs/store';
import { MastodonService, VisibilityEnum } from '../../../../services/mastodon.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { StatusWrapper } from '../../stream.component';
import { Status } from '../../../../services/models/mastodon.interfaces';
@Component({
selector: 'app-reply-to-status',
templateUrl: './reply-to-status.component.html',
styleUrls: ['./reply-to-status.component.scss']
})
export class ReplyToStatusComponent implements OnInit {
@Input() status: string = '';
@Input() statusReplyingToWrapper: StatusWrapper;
@Output() onClose = new EventEmitter();
@ViewChild('reply') replyElement: ElementRef;
private statusReplyingTo: Status;
selectedPrivacy = 'Public';
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
constructor(
private readonly store: Store,
private readonly mastodonService: MastodonService) { }
ngOnInit() {
this.statusReplyingTo = this.statusReplyingToWrapper.status;
this.status += `@${this.statusReplyingTo.account.acct} `;
for (const mention of this.statusReplyingTo.mentions) {
this.status += `@${mention.acct} `;
}
setTimeout(() => {
this.replyElement.nativeElement.focus();
}, 0);
}
onSubmit(): boolean {
const accounts = this.getRegisteredAccounts();
const selectedAccounts = accounts.filter(x => x.isSelected);
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
switch (this.selectedPrivacy) {
case 'Public':
visibility = VisibilityEnum.Public;
break;
case 'Unlisted':
visibility = VisibilityEnum.Unlisted;
break;
case 'Follows-only':
visibility = VisibilityEnum.Private;
break;
case 'DM':
visibility = VisibilityEnum.Direct;
break;
}
let spoiler = this.statusReplyingTo.spoiler_text;
for (const acc of selectedAccounts) {
this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler, this.statusReplyingTo.id)
.then((res: Status) => {
console.log(res);
this.status = '';
this.onClose.emit();
});
}
return false;
}
private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
onCtrlEnter(): boolean {
this.onSubmit();
return false;
}
}

View File

@ -1,9 +1,10 @@
<div class="reblog" *ngIf="reblog">
<a class="reblog__profile-link" href>{{ status.account.display_name }} <img *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
<a class="reblog__profile-link" href (click)="openAccount(status.account)">{{ status.account.display_name }} <img *ngIf="reblog" class="reblog__avatar"
src="{{ status.account.avatar }}" /></a> boosted
</div>
<div class="status">
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}">
<a href class="status__profile-link" title="{{displayedStatus.account.acct}}" (click)="openAccount(displayedStatus.account)">
<img [class.status__avatar--boosted]="reblog" class="status__avatar" src="{{ displayedStatus.account.avatar }}" />
<!-- <img *ngIf="reblog" class="status__avatar--reblog" src="{{ status.account.avatar }}" /> -->
<span class="status__name">
@ -13,23 +14,14 @@
</a>
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">{{
getCompactRelativeTime(status.created_at) }}</div>
<div class="status__content" innerHTML="{{displayedStatus.content}}"></div>
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
<div *ngIf="hasAttachments" class="attachments">
<app-attachements [attachments]="displayedStatus.media_attachments"></app-attachements>
</div>
<app-databinded-text class="status__content" [text]="displayedStatus.content"></app-databinded-text>
<div class="action-bar">
<a href class="action-bar__link"><ion-icon name="ios-undo"></ion-icon></a>
<a href class="action-bar__link"><ion-icon name="md-star"></ion-icon></a>
<a href class="action-bar__link"><ion-icon name="md-swap"></ion-icon></a>
<a href class="action-bar__link"><ion-icon name="ios-more"></ion-icon></a>
</div>
<!-- <div class="status_galery">
<p>
status.reblog: {{status.reblog}} <br />
status.media_attachments: {{status.media_attachments}}
</p>
</div> -->
<app-attachements *ngIf="hasAttachments" class="attachments" [attachments]="displayedStatus.media_attachments"></app-attachements>
<app-action-bar [statusWrapper]="statusWrapper" (replyEvent)="openReply()"></app-action-bar>
<app-reply-to-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()"></app-reply-to-status>
</div>

View File

@ -1,5 +1,5 @@
@import "variables";
$avatar-column-space: 70px;
.reblog {
position: relative;
margin: 5px 0 0 10px;
@ -76,12 +76,13 @@ $avatar-column-space: 70px;
&__content {
/*width: calc(100% - 50px);*/
word-wrap: break-word;
margin: 0px 10px 10px $avatar-column-space;
}
&__content p {
margin: 0;
font-size: 0.85em;
margin: 0 10px 0 $avatar-column-space;
display: block;
}
// &__content p {
// margin: 0 !important;
// font-size: 0.85em;
// }
&__created-at {
color: $status-secondary-color;
position: absolute;
@ -90,35 +91,8 @@ $avatar-column-space: 70px;
}
}
//Mastodon styling
:host ::ng-deep .status__content {
color: $status-primary-color;
& a,
.mention,
.ellipsis {
color: $status-links-color;
}
& .invisible {
display: none;
}
}
.attachments {
width: calc(100% - 80px);
margin: 0px 10px 10px $avatar-column-space;
display: block;
// width: calc(100% - 80px);
margin: 10px 10px 0 $avatar-column-space;
}
.action-bar {
// outline: 1px solid greenyellow; // height: 20px;
margin: 0px 10px 10px $avatar-column-space;
font-size: 24px;
&__link {
color: $status-secondary-color;
&:hover {
color: $status-links-color;
}
&:not(:last-child) {
margin-right: 15px;
}
}
}

View File

@ -1,8 +1,10 @@
import { Component, OnInit, Input, Inject, LOCALE_ID } from "@angular/core";
import { Status } from "../../../services/models/mastodon.interfaces";
import { Component, OnInit, Input, Output, Inject, LOCALE_ID, ElementRef, EventEmitter, Pipe, PipeTransform, ViewChild, Renderer2 } from "@angular/core";
import { Status, Account } from "../../../services/models/mastodon.interfaces";
import { formatDate } from '@angular/common';
import { stateNameErrorMessage } from "@ngxs/store/src/decorators/state";
import { StatusWrapper } from "../stream.component";
import { DomSanitizer } from '@angular/platform-browser'
@Component({
selector: "app-status",
@ -13,37 +15,67 @@ export class StatusComponent implements OnInit {
displayedStatus: Status;
reblog: boolean;
hasAttachments: boolean;
replyingToStatus: boolean;
private _status: Status;
@Input('status')
set status(value: Status) {
this._status = value;
@Output() browseAccount = new EventEmitter<Account>();
@Output() browseHashtag = new EventEmitter<string>();
@Output() browseThread = new EventEmitter<string>();
if(this.status.reblog){
private _statusWrapper: StatusWrapper;
status: Status;
@Input('statusWrapper')
set statusWrapper(value: StatusWrapper) {
this._statusWrapper = value;
this.status = value.status;
//TEST
//this.status.content += '<br/><br/><a href class="test">TEST</a>';
if (this.status.reblog) {
this.reblog = true;
this.displayedStatus = this._status.reblog;
this.displayedStatus = this.status.reblog;
} else {
this.displayedStatus = this._status;
this.displayedStatus = this.status;
}
if(!this.displayedStatus.account.display_name){
if (!this.displayedStatus.account.display_name) {
this.displayedStatus.account.display_name = this.displayedStatus.account.username;
}
if(this.displayedStatus.media_attachments && this.displayedStatus.media_attachments.length > 0){
if (this.displayedStatus.media_attachments && this.displayedStatus.media_attachments.length > 0) {
this.hasAttachments = true;
}
}
get status(): Status{
return this._status;
}
get statusWrapper(): StatusWrapper {
return this._statusWrapper;
}
constructor(@Inject(LOCALE_ID) private locale: string) { }
ngOnInit() {
ngOnInit() {
}
// ngAfterViewInit() {
// let el = this.contentElement.nativeElement.querySelector('.test');
// console.log(this.contentElement.nativeElement);
// console.log(el);
// if (el)
// this.renderer.listen(el, 'click', (el2) => {
// console.log(el2);
// console.warn('YOOOOO');
// return false;
// });
// }
openAccount(account: Account): boolean {
this.browseAccount.next(account);
return false;
}
getCompactRelativeTime(d: string): string {
@ -56,11 +88,27 @@ export class StatusComponent implements OnInit {
} else if (timeDelta < 60 * 60) {
return `${timeDelta / 60 | 0}m`;
} else if (timeDelta < 60 * 60 * 24) {
return `${timeDelta / (60 * 60)| 0}h`;
return `${timeDelta / (60 * 60) | 0}h`;
} else if (timeDelta < 60 * 60 * 24 * 31) {
return `${timeDelta / (60 * 60 * 24) | 0}d`;
}
return formatDate(date, 'MM/dd', this.locale);
}
openReply(): boolean {
this.replyingToStatus = !this.replyingToStatus;
return false;
}
closeReply(): boolean {
this.replyingToStatus = false;
return false;
}
test(): boolean {
console.warn('heeeeyaaa!');
return false;
}
}

View File

@ -0,0 +1,13 @@
<div class="stream-overlay">
<div class="stream-overlay__header">
<a href class="overlay-close" (click)="close()">CLOSE</a>
<a href class="overlay-previous">PREV</a>
<a href class="overlay-next">NEXT</a>
</div>
<!-- <div class="stream-overlay__title">
Account
</div> -->
<app-user-profile *ngIf="browseAccount" [currentAccount]="browseAccount"></app-user-profile>
<app-hashtag *ngIf="browseHashtag"></app-hashtag>
<app-thread *ngIf="browseThread"></app-thread>
</div>

View File

@ -0,0 +1,51 @@
@import "variables";
.stream-overlay {
// position: absolute;
// z-index: 50;
width: $stream-column-width;
height: calc(100%);
background-color: $column-color;
// 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;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StreamOverlayComponent } from './stream-overlay.component';
describe('StreamOverlayComponent', () => {
let component: StreamOverlayComponent;
let fixture: ComponentFixture<StreamOverlayComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StreamOverlayComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StreamOverlayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { Account } from "../../../services/models/mastodon.interfaces";
@Component({
selector: 'app-stream-overlay',
templateUrl: './stream-overlay.component.html',
styleUrls: ['./stream-overlay.component.scss']
})
export class StreamOverlayComponent implements OnInit {
private account: Account;
private thread: string;
private hashtag: string;
@Output() closeOverlay = new EventEmitter();
@Input('browseAccount')
set browseAccount(account: Account) {
this.account = account;
}
get browseAccount(): Account{
return this.account;
}
@Input('browseThread')
set browseThread(thread: string) {
this.thread = thread;
}
get browseThread(): string{
return this.thread;
}
@Input('browseHashtag')
set browseHashtag(hashtag: string) {
this.hashtag = hashtag;
}
get browseHashtag(): string{
return this.hashtag;
}
constructor() { }
ngOnInit() {
}
close(): boolean {
this.closeOverlay.next();
return false;
}
}

View File

@ -1,11 +1,17 @@
<div class="stream-column">
<div class="stream-column__stream-header">
<a href title="return to top" (click)="goToTop()"><h1>{{ streamElement.name.toUpperCase() }}</h1></a>
</div>
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()"> <!-- data-simplebar -->
<div class="stream-toots__status" *ngFor="let status of statuses">
<app-status [status]="status" ></app-status>
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
(closeOverlay)="closeOverlay()" [browseAccount]="overlayAccountToBrowse"></app-stream-overlay>
<div class="stream-column__stream-header">
<a href title="return to top" (click)="goToTop()">
<h1>{{ streamElement.name.toUpperCase() }}</h1>
</a>
</div>
</div>
</div>
<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)="browseAccount($event)"></app-status>
</div>
</div>
</div>

View File

@ -1,13 +1,15 @@
@import "variables";
.stream-column {
position: relative;
width: $stream-column-width;
height: calc(100%);
background-color: #0f111a;
background-color: $column-color;
margin: 0 0 0 $stream-column-separator;
&__stream-header {
width: calc(100%);
height: 30px;
background-color: black;
background-color: $column-header-background-color;
border-bottom: 1px solid black;
& h1 {
color: whitesmoke;
@ -47,4 +49,56 @@
background: lighten($color-primary, 5);
// -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
}
}
}
.stream-overlay {
position: absolute;
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;
// }

View File

@ -17,10 +17,13 @@ export class StreamComponent implements OnInit {
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: Status[] = [];
statuses: StatusWrapper[] = [];
private bufferStream: Status[] = [];
private bufferWasCleared: boolean;
overlayActive: boolean;
overlayAccountToBrowse: Account;
@Input()
set streamElement(streamElement: StreamElement) {
this._streamElement = streamElement;
@ -47,6 +50,26 @@ export class StreamComponent implements OnInit {
ngOnInit() {
}
browseAccount(account: Account): void {
this.overlayAccountToBrowse = account;
this.overlayActive = true;
}
browseHashtag(hashtag: any): void {
console.warn('browseHashtag');
console.warn(hashtag);
}
browseThread(thread: any): void {
console.warn('browseThread');
console.warn(thread);
}
closeOverlay(): void {
this.overlayAccountToBrowse = null;
this.overlayActive = false;
}
@ViewChild('statusstream') public statustream: ElementRef;
goToTop(): boolean {
this.loadBuffer();
@ -90,7 +113,8 @@ export class StreamComponent implements OnInit {
}
for (const status of this.bufferStream) {
this.statuses.unshift(status);
const wrapper = new StatusWrapper(status, this.account);
this.statuses.unshift(wrapper);
}
this.bufferStream.length = 0;
@ -100,10 +124,11 @@ export class StreamComponent implements OnInit {
this.isProcessingInfiniteScroll = true;
const lastStatus = this.statuses[this.statuses.length - 1];
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.id)
this.mastodonService.getTimeline(this.account, this._streamElement.type, lastStatus.status.id)
.then((status: Status[]) => {
for (const s of status) {
this.statuses.push(s);
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
})
.catch(err => {
@ -123,7 +148,8 @@ export class StreamComponent implements OnInit {
this.mastodonService.getTimeline(this.account, this._streamElement.type)
.then((results: Status[]) => {
for (const s of results) {
this.statuses.push(s);
const wrapper = new StatusWrapper(s, this.account);
this.statuses.push(wrapper);
}
});
}
@ -133,9 +159,10 @@ export class StreamComponent implements OnInit {
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (update) {
if (update.type === EventEnum.update) {
if (!this.statuses.find(x => x.id == update.status.id)) {
if (!this.statuses.find(x => x.status.id == update.status.id)) {
if (this.streamPositionnedAtTop) {
this.statuses.unshift(update.status);
const wrapper = new StatusWrapper(update.status, this.account);
this.statuses.unshift(wrapper);
} else {
this.bufferStream.push(update.status);
}
@ -146,7 +173,6 @@ export class StreamComponent implements OnInit {
this.checkAndCleanUpStream();
});
}
private checkAndCleanUpStream(): void {
if (this.streamPositionnedAtTop && this.statuses.length > 60) {
@ -159,4 +185,11 @@ export class StreamComponent implements OnInit {
}
}
}
export class StatusWrapper {
constructor(
public status: Status,
public provider: AccountInfo
) {}
}

View File

@ -0,0 +1,3 @@
<p>
thread works!
</p>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ThreadComponent } from './thread.component';
describe('ThreadComponent', () => {
let component: ThreadComponent;
let fixture: ComponentFixture<ThreadComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ThreadComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ThreadComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-thread',
templateUrl: './thread.component.html',
styleUrls: ['./thread.component.scss']
})
export class ThreadComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,9 @@
<div class="profile-header">
<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">@{{account.acct}}</h2>
</div>
<div class="profile-description" *ngIf="hasNote">
<p innerHTML="{{account.note}}"></p>
</div>

View File

@ -0,0 +1,48 @@
@import "variables";
.profile-header {
position: relative;
height: 140px;
overflow: hidden; // background-color: black;
border-bottom: 1px solid black;
& h2 {
font-size: $default-font-size;
}
&__header {
position: absolute;
// width: calc(100%);
width: calc(100%);
height: auto;
float: left;
display: block;
opacity: 0.3;
}
&__avatar {
position: absolute;
top: 15px;
left: 15px;
width: 80px;
border-radius: 50%; // border: 1px solid black;
// background-color: black;
}
&__display-name {
position: absolute;
top: 45px;
left: 115px;
// font-weight: bold;
color: white;
}
&__fullhandle {
position: absolute;
top: 105px;
left: 15px;
color: white;
}
}
.profile-description {
padding: 5px 10px 0 10px;
font-size: 13px;
border-bottom: 1px solid black;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UserProfileComponent } from './user-profile.component';
describe('UserProfileComponent', () => {
let component: UserProfileComponent;
let fixture: ComponentFixture<UserProfileComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UserProfileComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UserProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Component, OnInit, Input } from '@angular/core';
import { Account } from "../../../services/models/mastodon.interfaces";
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss']
})
export class UserProfileComponent implements OnInit {
account: Account;
hasNote: boolean;
@Input('currentAccount')
set currentAccount(account: Account) {
this.account = account;
this.hasNote = account && account.note && account.note !== '<p></p>';
console.warn('currentAccount');
console.warn(account);
}
constructor() { }
ngOnInit() {
}
}

View File

@ -0,0 +1,7 @@
<!-- https://loading.io/css/ -->
<div class="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>

View File

@ -0,0 +1,67 @@
//https://loading.io/css/
.lds-ellipsis {
display: inline-block;
position: relative;
//width: 64px;
//height: 64px;
width: 40px;
height: 20px;
}
.lds-ellipsis div {
position: absolute;
// top: 27px;
// width: 11px;
// height: 11px;
// top: 27px;
top: 8px;
width: 5px;
height: 5px;
border-radius: 50%;
background: #fff;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 6px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 6px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 16px;
// left: 26px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 25px;
// left: 45px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(10px, 0);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WaitingAnimationComponent } from './waiting-animation.component';
describe('WaitingAnimationComponent', () => {
let component: WaitingAnimationComponent;
let fixture: ComponentFixture<WaitingAnimationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ WaitingAnimationComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(WaitingAnimationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-waiting-animation',
templateUrl: './waiting-animation.component.html',
styleUrls: ['./waiting-animation.component.scss']
})
export class WaitingAnimationComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@ -5,10 +5,11 @@ import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum } from '../states/streams.state';
import { stat } from 'fs';
@Injectable()
export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { }
@ -115,6 +116,30 @@ export class MastodonService {
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Results>(route, { headers: headers }).toPromise()
}
reblog(account: AccountInfo, status: Status): Promise<Status> {
const route = `https://${account.instance}${this.apiRoutes.reblogStatus}`.replace('{0}', status.id);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
}
unreblog(account: AccountInfo, status: Status): Promise<Status> {
const route = `https://${account.instance}${this.apiRoutes.unreblogStatus}`.replace('{0}', status.id);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
}
favorite(account: AccountInfo, status: Status): any {
const route = `https://${account.instance}${this.apiRoutes.favouritingStatus}`.replace('{0}', status.id);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
}
unfavorite(account: AccountInfo, status: Status): any {
const route = `https://${account.instance}${this.apiRoutes.unfavouritingStatus}`.replace('{0}', status.id);
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
}
}
export enum VisibilityEnum {

View File

@ -116,15 +116,18 @@ export interface Status {
created_at: string;
reblogs_count: string;
favourites_count: string;
reblogged: string;
favourited: string;
sensitive: string;
reblogged: boolean;
favourited: boolean;
sensitive: boolean;
spoiler_text: string;
visibility: string;
media_attachments: Attachment[];
mentions: string;
tags: string;
mentions: Mention[];
tags: Tag[];
application: Application;
emojis: any[];
language: string;
pinned: boolean;
}
export interface Tag {
name: string;

View File

@ -2,24 +2,65 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sengi</title>
<base href="./">
<meta charset="utf-8">
<title>Sengi</title>
<base href="./">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<style>
.lds-ripple {
/* display: inline-block; */
margin: 30px auto;
position: relative;
width: 64px;
height: 64px;
}
.lds-ripple div {
position: absolute;
border: 4px solid #444f74;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 28px;
left: 28px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: -1px;
left: -1px;
width: 58px;
height: 58px;
opacity: 0;
}
}
</style>
</head>
<body>
<app-root>
loading...
</app-root>
<app-root>
<div class="lds-ripple">
<div></div>
<div></div>
</div>
</app-root>
<script src="https://unpkg.com/ionicons@4.4.2/dist/ionicons.js"></script>
<script src="https://unpkg.com/ionicons@4.4.2/dist/ionicons.js"></script>
</body>
</html>

5
src/sass/_commons.scss Normal file
View File

@ -0,0 +1,5 @@
.waiting-icon {
width: 40px;
display: block;
margin: 5px auto;
}

View File

@ -1,4 +1,5 @@
.panel{
width: 100%;
padding: 10px 10px 0 7px;
font-size: $small-font-size;
&__title {
@ -6,4 +7,4 @@
text-transform: uppercase;
margin: 6px 0 12px 0;
}
}
}

View File

@ -4,28 +4,33 @@ $font-link-primary: #595c67;
$font-link-primary-hover: #8f93a2;
$color-primary: #141824;
$color-secondary: #090b10;
$column-color: #0f111a;
$column-header-background-color: black;
$default-font-size: 15px;
$small-font-size: 12px;
$btn-primary-color: #515a62;
$btn-primary-color: #254d6f;
// $btn-primary-color: #515a62;
// $btn-primary-color: #254d6f;
$btn-primary-color: #444f74;
$btn-primary-color-hover: darken($btn-primary-color, 10);
$btn-primary-font-color: white;
// TEST 1
$status-primary-color: #fff;
$status-secondary-color: #353e64;
// $status-secondary-color: #353e64;
$status-secondary-color: #4e5572;
$status-links-color: #d9e1e8;
// $status-primary-color : #8f93a2;
// $status-primary-color : lighten(#8f93a2, 30);
// $status-links-color : #b2ccd6;
$boost-color : #5098eb;
$favorite-color: #ffc16f;
// Block dispositions
$stream-selector-height: 30px;
$stream-column-separator: 7px;
$stream-column-width: 320px;
$avatar-column-space: 70px;
//Bootstrap cuistomization
$enable-rounded : false;
$enable-rounded: false;