diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 53f78a19..c3227cbe 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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: [ diff --git a/src/app/components/stream/status/action-bar/action-bar.component.html b/src/app/components/stream/status/action-bar/action-bar.component.html new file mode 100644 index 00000000..2f50011b --- /dev/null +++ b/src/app/components/stream/status/action-bar/action-bar.component.html @@ -0,0 +1,21 @@ +
+ + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/action-bar.component.scss b/src/app/components/stream/status/action-bar/action-bar.component.scss new file mode 100644 index 00000000..ae753be3 --- /dev/null +++ b/src/app/components/stream/status/action-bar/action-bar.component.scss @@ -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); + } +} \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/action-bar.component.spec.ts b/src/app/components/stream/status/action-bar/action-bar.component.spec.ts new file mode 100644 index 00000000..be44980a --- /dev/null +++ b/src/app/components/stream/status/action-bar/action-bar.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ActionBarComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ActionBarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/stream/status/action-bar/action-bar.component.ts b/src/app/components/stream/status/action-bar/action-bar.component.ts new file mode 100644 index 00000000..f0fe0602 --- /dev/null +++ b/src/app/components/stream/status/action-bar/action-bar.component.ts @@ -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; + 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 = 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 = 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 = this.selectedAccounts[0]; + if (selectedAccount) { + this.isBoosted = this.bootedStatePerAccountId[selectedAccount.id]; + } else { + this.isBoosted = false; + } + } + + private checkIfFavorited() { + const selectedAccount = 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 = this.store.snapshot().registeredaccounts.accounts; + return regAccounts; + } +} diff --git a/src/app/components/stream/status/status.component.html b/src/app/components/stream/status/status.component.html index 13612b9b..ce7f40d0 100644 --- a/src/app/components/stream/status/status.component.html +++ b/src/app/components/stream/status/status.component.html @@ -1,5 +1,6 @@
@@ -15,21 +16,7 @@ getCompactRelativeTime(status.created_at) }}
-
- -
+ -
- - - - -
- - + \ No newline at end of file diff --git a/src/app/components/stream/status/status.component.scss b/src/app/components/stream/status/status.component.scss index cd7dd691..4db923be 100644 --- a/src/app/components/stream/status/status.component.scss +++ b/src/app/components/stream/status/status.component.scss @@ -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; - } - } -} \ No newline at end of file diff --git a/src/app/components/stream/status/status.component.ts b/src/app/components/stream/status/status.component.ts index b0c36d3d..ca495536 100644 --- a/src/app/components/stream/status/status.component.ts +++ b/src/app/components/stream/status/status.component.ts @@ -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; } diff --git a/src/app/components/stream/stream.component.html b/src/app/components/stream/stream.component.html index fbfd0357..8bb78cbf 100644 --- a/src/app/components/stream/stream.component.html +++ b/src/app/components/stream/stream.component.html @@ -3,8 +3,8 @@

{{ streamElement.name.toUpperCase() }}

-
- +
+
diff --git a/src/app/components/stream/stream.component.ts b/src/app/components/stream/stream.component.ts index 7cb6b257..10b94a81 100644 --- a/src/app/components/stream/stream.component.ts +++ b/src/app/components/stream/stream.component.ts @@ -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 + ) {} } \ No newline at end of file diff --git a/src/app/services/mastodon.service.ts b/src/app/services/mastodon.service.ts index a189e679..39c82a3f 100644 --- a/src/app/services/mastodon.service.ts +++ b/src/app/services/mastodon.service.ts @@ -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(route, { headers: headers }).toPromise() } + + reblog(account: AccountInfo, status: Status): Promise { + 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(route, null, { headers: headers }).toPromise() + } + + unreblog(account: AccountInfo, status: Status): Promise { + 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(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(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(route, null, { headers: headers }).toPromise() + } } export enum VisibilityEnum { diff --git a/src/app/services/models/mastodon.interfaces.ts b/src/app/services/models/mastodon.interfaces.ts index 2d4aeb0a..e2b50740 100644 --- a/src/app/services/models/mastodon.interfaces.ts +++ b/src/app/services/models/mastodon.interfaces.ts @@ -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; diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss index ae8164e5..e8b45f14 100644 --- a/src/sass/_variables.scss +++ b/src/sass/_variables.scss @@ -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; \ No newline at end of file +$enable-rounded: false; \ No newline at end of file