Merge pull request #16 from NicolasConstant/feature_open-user-profile
Feature open user profile
This commit is contained in:
commit
3b41795a83
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -1,6 +1,7 @@
|
|||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "panel";
|
||||
@import "commons";
|
||||
|
||||
$separator-color:$color-primary;
|
||||
|
||||
|
|
|
@ -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; });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
hashtag works!
|
||||
</p>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<div #content class="content" innerHTML="{{processedText}}" (click)="selectText()"></div>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
// }
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
// }
|
|
@ -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
|
||||
) {}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<p>
|
||||
thread works!
|
||||
</p>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<!-- https://loading.io/css/ -->
|
||||
<div class="lds-ellipsis">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
.waiting-icon {
|
||||
width: 40px;
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
Loading…
Reference in New Issue