Merge pull request #14 from NicolasConstant/feature_add-boosts-and-favs

Feature add boosts and favs
This commit is contained in:
Nicolas Constant 2018-10-12 21:09:45 -04:00 committed by GitHub
commit 015ac2a184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 367 additions and 66 deletions

View File

@ -32,6 +32,7 @@ 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';
const routes: Routes = [
@ -58,6 +59,7 @@ const routes: Routes = [
SettingsComponent,
AddNewAccountComponent,
SearchComponent,
ActionBarComponent,
WaitingAnimationComponent
],
imports: [

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,190 @@
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Store } from '@ngxs/store';
import { StatusWrapper } from '../../stream.component';
import { MastodonService } from '../../../../services/mastodon.service';
import { AccountInfo } from '../../../../states/accounts.state';
import { Observable, Subscription } from 'rxjs';
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;
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 {
console.warn('reply');
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

@ -1,5 +1,6 @@
<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>{{ status.account.display_name }} <img *ngIf="reblog" class="reblog__avatar"
src="{{ status.account.avatar }}" /></a> boosted
</div>
<div class="status">
@ -15,21 +16,7 @@
getCompactRelativeTime(status.created_at) }}</div>
<div class="status__content" innerHTML="{{displayedStatus.content}}"></div>
<div *ngIf="hasAttachments" class="attachments">
<app-attachements [attachments]="displayedStatus.media_attachments"></app-attachements>
</div>
<app-attachements *ngIf="hasAttachments" class="attachments" [attachments]="displayedStatus.media_attachments"></app-attachements>
<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-action-bar [statusWrapper]="statusWrapper"></app-action-bar>
</div>

View File

@ -1,5 +1,5 @@
@import "variables";
$avatar-column-space: 70px;
.reblog {
position: relative;
margin: 5px 0 0 10px;
@ -76,12 +76,12 @@ $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;
}
// &__content p {
// margin: 0 !important;
// font-size: 0.85em;
// }
&__created-at {
color: $status-secondary-color;
position: absolute;
@ -90,6 +90,10 @@ $avatar-column-space: 70px;
}
}
// .attachments {
// }
//Mastodon styling
:host ::ng-deep .status__content {
color: $status-primary-color;
@ -101,24 +105,15 @@ $avatar-column-space: 70px;
& .invisible {
display: none;
}
& p {
margin: 0px;
//font-size: .9em;
// font-size: 14px;
}
}
.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

@ -2,6 +2,7 @@ import { Component, OnInit, Input, Inject, LOCALE_ID } from "@angular/core";
import { Status } from "../../../services/models/mastodon.interfaces";
import { formatDate } from '@angular/common';
import { stateNameErrorMessage } from "@ngxs/store/src/decorators/state";
import { StatusWrapper } from "../stream.component";
@Component({
@ -14,16 +15,18 @@ export class StatusComponent implements OnInit {
reblog: boolean;
hasAttachments: boolean;
private _status: Status;
@Input('status')
set status(value: Status) {
this._status = value;
private _statusWrapper: StatusWrapper;
status: Status;
@Input('statusWrapper')
set statusWrapper(value: StatusWrapper) {
this._statusWrapper = value;
this.status = value.status;
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){
@ -36,8 +39,8 @@ export class StatusComponent implements OnInit {
}
get status(): Status{
return this._status;
get statusWrapper(): StatusWrapper{
return this._statusWrapper;
}

View File

@ -3,8 +3,8 @@
<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>
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
<app-status [statusWrapper]="statusWrapper" ></app-status>
</div>
</div>
</div>

View File

@ -17,7 +17,7 @@ export class StreamComponent implements OnInit {
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
statuses: Status[] = [];
statuses: StatusWrapper[] = [];
private bufferStream: Status[] = [];
private bufferWasCleared: boolean;
@ -90,7 +90,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 +101,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 +125,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 +136,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);
}
@ -159,4 +163,11 @@ export class StreamComponent implements OnInit {
}
}
}
export class StatusWrapper {
constructor(
public status: Status,
public provider: AccountInfo
) {}
}

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,16 @@ 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;
application: Application;
emojis: any[];
}
export interface Tag {
name: string;

View File

@ -6,13 +6,11 @@ $color-primary: #141824;
$color-secondary: #090b10;
$default-font-size: 15px;
$small-font-size: 12px;
$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;
@ -21,11 +19,12 @@ $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;