feat: adds authentication to ngrx store
This commit is contained in:
30
angular.json
30
angular.json
@@ -36,7 +36,14 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [{
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "projects/mastolists/src/environments/environment.ts",
|
||||
"with": "projects/mastolists/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "3mb"
|
||||
@@ -56,6 +63,27 @@
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"staging": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "projects/mastolists/src/environments/environment.ts",
|
||||
"with": "projects/mastolists/src/environments/environment.staging.ts"
|
||||
}
|
||||
],
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "3mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build:prod": "ng build --configuration=production",
|
||||
"build:staging": "ng build --configuration=staging",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {map, Observable} from "rxjs";
|
||||
import {catchError, map, Observable, of, throwError} from "rxjs";
|
||||
import {RegisterAppResponse} from "../interfaces/responses/register_app_response";
|
||||
import {GetAccessTokenResponse} from "../interfaces/responses/get_access_token_response";
|
||||
import {VerifyCredentialsResponse} from "../interfaces/responses/verify_credentials_response";
|
||||
@@ -31,7 +31,8 @@ export class MastodonApiAuthenticationService {
|
||||
const url = `https://${instance}/api/v1/apps`;
|
||||
return this.mastodonApiService
|
||||
.post<RegisterAppResponse>(url, parameters)
|
||||
.pipe(map((response) => {
|
||||
.pipe(
|
||||
map((response) => {
|
||||
return <RegisteredApp>{
|
||||
id: response.id,
|
||||
name: response.name,
|
||||
@@ -41,7 +42,9 @@ export class MastodonApiAuthenticationService {
|
||||
clientSecret: response.client_secret,
|
||||
vapidKey: response.vapid_key,
|
||||
}
|
||||
}));
|
||||
}),
|
||||
catchError((error) => throwError(error)),
|
||||
);
|
||||
}
|
||||
|
||||
authorizeUser(instance: string, clientId: string, redirectUrl: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {AuthGuard} from './shared/guards/auth.guard';
|
||||
import {AuthGuard} from './authorization/guards/auth.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const routes: Routes = [
|
||||
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
path: 'authorize',
|
||||
loadChildren: () => import('./authorization/authorization.module').then(m => m.AuthorizationModule),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<a [routerLink]="['/']">
|
||||
<img height="40px" src="assets/images/novaloop.png" alt="Novaloop favicon">
|
||||
</a>
|
||||
<h2>Mastolists</h2>
|
||||
<h2>{{appName}}</h2>
|
||||
</div>
|
||||
<!--add class button-responsive to the button-->
|
||||
<button nbButton ghost class="button-responsive" [nbContextMenu]="navigationItems">
|
||||
@@ -33,6 +33,7 @@
|
||||
Novaloop AG<br/>
|
||||
Niederdorfstrasse 88<br/>
|
||||
8001 Zürich<br/>
|
||||
<span class="version-number">v{{version}}</span>
|
||||
</div>
|
||||
<div class="col col-center">
|
||||
<div class="mastodon-link">
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
span.version-number {
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
img.mastodon-logo {
|
||||
width: 15px;
|
||||
margin-right: 10px;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {Component, isDevMode} from '@angular/core';
|
||||
import {Store} from "@ngrx/store";
|
||||
import {MastodonApiActions} from "./shared/state/store/actions";
|
||||
import {PersistentStore} from "./shared/state/persistent/persistent-store.service";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {fromApplication, fromAuthorize, selectIsLoggedIn} from "./shared/state/store";
|
||||
import {environment} from "../environments/environment";
|
||||
// @ts-ignore
|
||||
import packageJson from '../../../../package.json';
|
||||
import {tap} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -9,10 +12,11 @@ import {PersistentStore} from "./shared/state/persistent/persistent-store.servic
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'mastolists';
|
||||
appName = environment.appName;
|
||||
version = packageJson.version;
|
||||
|
||||
navigationItems = [
|
||||
{title: 'Authorize', link: '/auth'},
|
||||
{title: 'Authorize', link: '/authorize'},
|
||||
{title: 'Stats', link: '/sync'},
|
||||
{title: 'Edit Lists', link: '/lists'},
|
||||
{title: 'List view', link: '/followings/list'},
|
||||
@@ -20,10 +24,15 @@ export class AppComponent {
|
||||
{title: 'Table View', link: '/followings/table'},
|
||||
];
|
||||
|
||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
||||
if (this.persistentStore.isAuthorized() && !isDevMode()) {
|
||||
this.store.dispatch(MastodonApiActions.loadLists());
|
||||
this.store.dispatch(MastodonApiActions.loadFollowings());
|
||||
constructor(private store: Store) {
|
||||
this.store.dispatch(fromAuthorize.loadLocalStorage());
|
||||
this.store.pipe(
|
||||
select(selectIsLoggedIn),
|
||||
).subscribe((loggedIn) => {
|
||||
if (loggedIn && !isDevMode()) {
|
||||
this.store.dispatch(fromApplication.loadLists());
|
||||
this.store.dispatch(fromApplication.loadFollowings());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,25 @@ import {AppComponent} from './app.component';
|
||||
import {MastodonApiModule} from "../../../mastodon-api/src/lib/mastodon-api.module";
|
||||
import {
|
||||
NbActionsModule,
|
||||
NbContextMenuModule, NbDialogModule,
|
||||
NbContextMenuModule,
|
||||
NbDialogModule,
|
||||
NbGlobalPhysicalPosition,
|
||||
NbLayoutModule,
|
||||
NbMenuModule, NbProgressBarModule,
|
||||
NbMenuModule,
|
||||
NbThemeModule,
|
||||
NbToastrModule
|
||||
} from "@nebular/theme";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {SharedModule} from './shared/shared.module';
|
||||
import {NbEvaIconsModule} from "@nebular/eva-icons";
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
import {ActionReducer, StoreModule} from '@ngrx/store';
|
||||
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
|
||||
import {applicationStateReducers} from "./shared/state/store/reducers";
|
||||
import {applicationStateReducer, authenticationStateReducer} from "./shared/state/store/reducers";
|
||||
import {EffectsModule} from "@ngrx/effects";
|
||||
import {ApplicationEffects} from "./shared/state/store/effects";
|
||||
import {NgProgressModule} from "ngx-progressbar";
|
||||
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
||||
import {AuthorizationModule} from "./authorization/authorization.module";
|
||||
import {ApplicationStateEffects, AuthorizationStateEffects, fromAuthorize} from "./shared/state/store";
|
||||
|
||||
const toastrConfig = {
|
||||
duration: 3000,
|
||||
@@ -31,6 +33,28 @@ const toastrConfig = {
|
||||
destroyByClick: true,
|
||||
};
|
||||
|
||||
export function handlePersistentState(reducer: ActionReducer<any>): ActionReducer<any> {
|
||||
return function (state, action) {
|
||||
let nextState = reducer(state, action);
|
||||
if (action.type === fromAuthorize.logout.type) {
|
||||
localStorage.removeItem('authenticationState');
|
||||
}
|
||||
if (
|
||||
action.type === fromAuthorize.registerApplicationSuccess.type ||
|
||||
action.type === fromAuthorize.getAccessTokenSuccess.type
|
||||
) {
|
||||
localStorage.setItem('authenticationState', JSON.stringify(nextState));
|
||||
}
|
||||
if (action.type === fromAuthorize.loadLocalStorage.type) {
|
||||
const savedState = JSON.parse(localStorage.getItem('authenticationState') || '{}');
|
||||
nextState = {...nextState, ...savedState};
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
}
|
||||
|
||||
export const metaReducers = [handlePersistentState];
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [
|
||||
@@ -41,9 +65,13 @@ const toastrConfig = {
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
MastodonApiModule.forRoot(),
|
||||
AuthorizationModule.forRoot(),
|
||||
StoreModule.forRoot({}),
|
||||
StoreModule.forFeature('applicationState', applicationStateReducers),
|
||||
EffectsModule.forRoot(ApplicationEffects),
|
||||
StoreModule.forFeature('authentication', authenticationStateReducer, {metaReducers}),
|
||||
StoreModule.forFeature('application', applicationStateReducer),
|
||||
EffectsModule.forRoot(),
|
||||
EffectsModule.forFeature(ApplicationStateEffects),
|
||||
EffectsModule.forFeature(AuthorizationStateEffects),
|
||||
StoreDevtoolsModule.instrument({
|
||||
maxAge: 25,
|
||||
logOnly: false,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {ModuleWithProviders, NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AuthorizationRoutingModule} from "./authorization-routing.module";
|
||||
import {AuthorizeComponent} from './authorize/authorize.component';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {NbSpinnerModule} from "@nebular/theme";
|
||||
import {AuthGuard} from "./guards/auth.guard";
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -20,4 +21,12 @@ import {NbSpinnerModule} from "@nebular/theme";
|
||||
]
|
||||
})
|
||||
export class AuthorizationModule {
|
||||
static forRoot(): ModuleWithProviders<AuthorizationModule> {
|
||||
return {
|
||||
ngModule: AuthorizationModule,
|
||||
providers: [
|
||||
AuthGuard,
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,27 @@
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<div [formGroup]="serverForm">
|
||||
<input nbInput id="serverUrl" type="text" placeholder="mastodon.social" [formControlName]="'instance'">
|
||||
<button class="authorize-button" nbButton type="button"
|
||||
[nbSpinner]="authorizing"
|
||||
(click)="registerApplication()">Authorize
|
||||
<input nbInput id="serverUrl" type="text" placeholder="mastodon.social" [formControlName]="'instance'" (keydown.enter)="registerApplication()">
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(registeringApplication$ | async)!"
|
||||
[disabled]="serverForm.invalid || (applicationRegistered$ | async) === true || (registeringApplication$ | async)"
|
||||
(click)="registerApplication()">Register Application
|
||||
</button>
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(authorizingUser$ | async)!"
|
||||
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async) || (loggedIn$ | async)"
|
||||
(click)="authorizeUser()">Authorize User
|
||||
</button>
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(authorizingUser$ | async)!"
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async)"
|
||||
(click)="logout()">Logout
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isAuthorized.value">
|
||||
<p>User is authorized with {{currentInstance?.instanceName}}</p>
|
||||
<p>Next up: <a routerLink="/sync">Sync</a></p>
|
||||
<div *ngIf="(loggedIn$ | async) && (instanceName$ | async) as instanceName">
|
||||
<p>User is authorized with {{instanceName}}</p>
|
||||
<p>Next up: Wait for <a routerLink="/sync">Sync</a> and manage</p>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
.authorize-button {
|
||||
margin: 20px;
|
||||
.button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
.authorize-button {
|
||||
.button {
|
||||
margin-left: 0;
|
||||
margin-top: 20px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {ActivatedRoute, Params, Router} from "@angular/router";
|
||||
import {BehaviorSubject, filter, Subscription, take} from "rxjs";
|
||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
||||
import {MastodonApiAccountsService, MastodonApiAuthenticationService, MastodonApiListsService} from "projects/mastodon-api/src/public-api";
|
||||
import {Instance} from "../../shared/state/persistent/state";
|
||||
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||
import {ActivatedRoute, Params} from "@angular/router";
|
||||
import {concat, filter, map, merge, Observable, take, tap, withLatestFrom, zip, zipWith} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {
|
||||
selectAuthorizingUserFlag,
|
||||
selectCurrentInstance,
|
||||
fromAuthorize,
|
||||
selectIsLoggedIn,
|
||||
selectRegisteringApplicationFlag,
|
||||
selectDataForAuthorizationFlow, selectApplicationRegisteredFlag, selectCurrentInstanceWithApplicationRegisteredState
|
||||
} from "../../shared/state/store";
|
||||
|
||||
@Component({
|
||||
selector: 'app-authorize',
|
||||
@@ -12,109 +18,86 @@ import {Instance} from "../../shared/state/persistent/state";
|
||||
styleUrls: ['./authorize.component.scss']
|
||||
})
|
||||
export class AuthorizeComponent implements OnInit, OnDestroy {
|
||||
subscriptions: Subscription[] = [];
|
||||
currentInstance?: Instance;
|
||||
accessToken?: string;
|
||||
accountId?: string;
|
||||
isAuthorized: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
authorizing: boolean = false;
|
||||
|
||||
registeringApplication$: Observable<boolean>;
|
||||
applicationRegistered$: Observable<boolean>;
|
||||
authorizingUser$: Observable<boolean>;
|
||||
loggedIn$: Observable<boolean>;
|
||||
instanceName$: Observable<string | undefined>;
|
||||
serverForm = new FormGroup({
|
||||
instance: new FormControl(''),
|
||||
instance: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(private mastodonApiAuthService: MastodonApiAuthenticationService,
|
||||
private store: PersistentStore,
|
||||
private mastodonApiAccountsService: MastodonApiAccountsService,
|
||||
private mastodonApiListsService: MastodonApiListsService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,) {
|
||||
this.subscriptions.push(this.store
|
||||
.select('currentInstance')
|
||||
.subscribe((currentInstance) => {
|
||||
this.currentInstance = <Instance>currentInstance
|
||||
this.accessToken = this.currentInstance?.accessToken;
|
||||
this.accountId = this.currentInstance?.accountId;
|
||||
this.isAuthorized.next(!!(this.currentInstance && this.accessToken && this.accountId));
|
||||
constructor(private store: Store, private route: ActivatedRoute) {
|
||||
this.registeringApplication$ = this.store.pipe(select(selectRegisteringApplicationFlag));
|
||||
this.applicationRegistered$ = this.store.pipe(select(selectApplicationRegisteredFlag));
|
||||
this.authorizingUser$ = this.store.pipe(select(selectAuthorizingUserFlag));
|
||||
this.loggedIn$ = this.store.pipe(select(selectIsLoggedIn));
|
||||
this.instanceName$ = this.store.pipe(
|
||||
select(selectCurrentInstanceWithApplicationRegisteredState),
|
||||
tap((result) => {
|
||||
if (result.instanceName !== undefined && result.applicationRegistered) {
|
||||
this.serverForm.get('instance')?.setValue(result.instanceName);
|
||||
this.serverForm.get('instance')?.disable();
|
||||
} else {
|
||||
this.serverForm.get('instance')?.enable();
|
||||
}
|
||||
}),
|
||||
map((result) => result.instanceName)
|
||||
);
|
||||
zip(
|
||||
this.route.queryParams,
|
||||
this.instanceName$,
|
||||
this.loggedIn$,
|
||||
)
|
||||
.pipe(
|
||||
filter(([params, instanceName, isLoggedIn]) => params['code'] && instanceName !== undefined && !isLoggedIn),
|
||||
take(1),
|
||||
).subscribe(([params, instanceName, isLoggedIn]) => {
|
||||
this.store.pipe(
|
||||
select(selectDataForAuthorizationFlow),
|
||||
take(1),
|
||||
).subscribe((data) => {
|
||||
this.store.dispatch(fromAuthorize.getAccessToken({
|
||||
code: params['code'],
|
||||
instanceName: data.instanceName!,
|
||||
clientId: data.clientId!,
|
||||
clientSecret: data.clientSecret!,
|
||||
redirectUrl: data.redirectUrl!,
|
||||
}));
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
filter((params: Params) => params['code']),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(params => {
|
||||
if (this.currentInstance && !this.accessToken && params['code']) {
|
||||
this.mastodonApiAuthService
|
||||
.getAccessToken(this.currentInstance.instanceName, this.currentInstance.clientId, this.currentInstance.clientSecret, this.currentInstance.redirectUrl, params['code'])
|
||||
.pipe(take(1))
|
||||
.subscribe(accessToken => {
|
||||
this.store.set(
|
||||
'currentInstance',
|
||||
{...this.currentInstance, 'accessToken': accessToken}
|
||||
);
|
||||
this.mastodonApiAuthService.verifyCredentials(this.currentInstance!.instanceName, accessToken)
|
||||
.pipe(take(1))
|
||||
.subscribe((accountId) => {
|
||||
this.store.set(
|
||||
'currentInstance',
|
||||
{...this.currentInstance, 'accountId': accountId}
|
||||
);
|
||||
this.router.navigate(['/sync']);
|
||||
this.isAuthorized.next(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async registerApplication() {
|
||||
this.authorizing = true;
|
||||
this.subscriptions.push(this.store
|
||||
.select<Instance>('currentInstance')
|
||||
.subscribe((currentInstance) => {
|
||||
this.authorizing = false;
|
||||
let instanceName = this.serverForm.value.instance ?? '';
|
||||
if (instanceName === '') {
|
||||
return;
|
||||
const instanceName = this.serverForm.get('instance')?.value;
|
||||
if (instanceName) {
|
||||
const redirectUrl = window.location.protocol + '//' + window.location.host + window.location.pathname;
|
||||
this.store.dispatch(fromAuthorize.registerApplication({instanceName, redirectUrl}));
|
||||
}
|
||||
if (currentInstance?.instanceName === instanceName) {
|
||||
this.mastodonApiAuthService
|
||||
.authorizeUser(currentInstance.instanceName, currentInstance.clientId, currentInstance.redirectUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// let redirectUrl = window.location.toString();
|
||||
let redirectUrl = window.location.protocol + '//' + window.location.host + window.location.pathname;
|
||||
this.authorizing = true;
|
||||
this.mastodonApiAuthService
|
||||
.createApp(instanceName, 'Mastolists', redirectUrl, 'https://mastolists.novaloop.cloud')
|
||||
.pipe(take(1))
|
||||
.subscribe(res => {
|
||||
this.authorizing = false;
|
||||
this.store.set(
|
||||
'currentInstance',
|
||||
<Instance>{
|
||||
id: res.id,
|
||||
instanceName: instanceName,
|
||||
appName: res.name,
|
||||
website: res.website,
|
||||
redirectUrl: res.redirectUri,
|
||||
clientId: res.clientId,
|
||||
clientSecret: res.clientSecret,
|
||||
}
|
||||
);
|
||||
this.mastodonApiAuthService.authorizeUser(instanceName, res.clientId, res.redirectUri);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
authorizeUser() {
|
||||
this.store.pipe(
|
||||
select(selectDataForAuthorizationFlow),
|
||||
take(1),
|
||||
).subscribe((data) => {
|
||||
this.store.dispatch(fromAuthorize.authorizeUser({
|
||||
instanceName: data.instanceName!,
|
||||
clientId: data.clientId!,
|
||||
redirectUrl: data.redirectUrl!,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.store.dispatch(fromAuthorize.logout());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from "@angular/router";
|
||||
import {Observable, tap} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectIsLoggedIn} from "../../shared/state/store";
|
||||
import {MastodonApiAuthenticationService} from 'projects/mastodon-api/src/public-api';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(private store: Store,
|
||||
private router: Router,
|
||||
private mastodonApiAuthService: MastodonApiAuthenticationService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
return this.store
|
||||
.pipe(
|
||||
select(selectIsLoggedIn),
|
||||
tap((isLoggedIn) => {
|
||||
if (!isLoggedIn) {
|
||||
this.router.navigate(['/authorize']);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
||||
import {Account, List, MastodonApiListsService} from 'projects/mastodon-api/src/public-api';
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {ListService} from "../../shared/services/list.service";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectFilteredFollowingsWithLists, selectFollowings, selectFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
||||
import {FiltersActions, ListActions, MastodonApiActions} from "../../shared/state/store/actions";
|
||||
import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
|
||||
@Component({
|
||||
selector: 'app-list',
|
||||
@@ -23,11 +19,11 @@ export class ListComponent {
|
||||
}
|
||||
|
||||
addAccountToSelectedList(accountId: string, select: HTMLSelectElement) {
|
||||
this.store.dispatch(ListActions.addAccountToList({accountId, listId: select.value}));
|
||||
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId: select.value}));
|
||||
}
|
||||
|
||||
removeAccountFromList(accountId: string, listId: string) {
|
||||
this.store.dispatch(ListActions.removeAccountFromList({accountId, listId}));
|
||||
this.store.dispatch(fromListViewPage.removeAccountFromList({accountId, listId}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import {Component, TemplateRef, ViewChild} from '@angular/core';
|
||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
||||
import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
import {List} from "../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
import {ListService} from "../../shared/services/list.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
||||
import {ListActions} from "../../shared/state/store/actions";
|
||||
import {NbDialogService} from "@nebular/theme";
|
||||
import {fromMatrixViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-matrix',
|
||||
@@ -36,9 +32,9 @@ export class MatrixComponent {
|
||||
|
||||
onCheckedChange($event: boolean, accountId: string, listId: string) {
|
||||
if ($event) {
|
||||
this.store.dispatch(ListActions.addAccountToList({accountId, listId}));
|
||||
this.store.dispatch(fromMatrixViewPage.addAccountToList({accountId, listId}));
|
||||
} else {
|
||||
this.store.dispatch(ListActions.removeAccountFromList({accountId, listId}));
|
||||
this.store.dispatch(fromMatrixViewPage.removeAccountFromList({accountId, listId}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/acc
|
||||
import {List} from "../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
||||
import {ListActions} from "../../shared/state/store/actions";
|
||||
import {fromTableViewPage} from "../../shared/state/store";
|
||||
|
||||
interface DataGridRow {
|
||||
id: string;
|
||||
@@ -49,10 +49,10 @@ export class TableComponent {
|
||||
}
|
||||
|
||||
addAccountToSelectedList(accountId: string, listId: string) {
|
||||
this.store.dispatch(ListActions.addAccountToList({accountId, listId}));
|
||||
this.store.dispatch(fromTableViewPage.addAccountToList({accountId, listId}));
|
||||
}
|
||||
|
||||
removeAccountFromList(accountId: string, listId: string) {
|
||||
this.store.dispatch(ListActions.removeAccountFromList({accountId, listId}));
|
||||
this.store.dispatch(fromTableViewPage.removeAccountFromList({accountId, listId}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p>Organize your Followings into lists.</p>
|
||||
|
||||
<ul>
|
||||
<li><a [routerLink]="['/auth']">Authorize</a> with your instance</li>
|
||||
<li><a [routerLink]="['/authorize']">Authorize</a> with your instance</li>
|
||||
<li><a [routerLink]="['/sync']">Sync</a> your lists and followings to local storage</li>
|
||||
<li>Select the <a [routerLink]="['/followings/list']">List view</a> to add and remove users from lists</li>
|
||||
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<table>
|
||||
<table *ngIf="(instanceName$ | async) as instanceName">
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Title</th>
|
||||
|
||||
@@ -2,10 +2,9 @@ import {Component} from '@angular/core';
|
||||
import {List} from 'projects/mastodon-api/src/public-api';
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectLists} from "../../shared/state/store/selectors";
|
||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
||||
import {ListActions} from "../../shared/state/store/actions";
|
||||
import {selectCurrentInstance, selectLists} from "../../shared/state/store/selectors";
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {fromListsPage} from '../../shared/state/store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lists',
|
||||
@@ -15,28 +14,28 @@ import {FormControl, FormGroup} from "@angular/forms";
|
||||
export class ListsComponent {
|
||||
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
instanceName: string;
|
||||
instanceName$: Observable<string | undefined>;
|
||||
creatingList: boolean = false;
|
||||
|
||||
newListForm = new FormGroup({
|
||||
title: new FormControl(''),
|
||||
});
|
||||
|
||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
||||
this.instanceName = persistentStore.value.currentInstance.instanceName;
|
||||
constructor(private store: Store) {
|
||||
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
}
|
||||
|
||||
listNameChanged(id: string, event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const newTitle = target.value;
|
||||
this.store.dispatch(ListActions.updateList({listId: id, newTitle}));
|
||||
this.store.dispatch(fromListsPage.updateList({listId: id, newTitle}));
|
||||
}
|
||||
|
||||
createList() {
|
||||
let title = this.newListForm.value.title ?? '';
|
||||
if (title.length > 0) {
|
||||
this.store.dispatch(ListActions.createList({title}));
|
||||
this.store.dispatch(fromListsPage.createList({title}));
|
||||
this.newListForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {Component, OnDestroy} from '@angular/core';
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {FiltersActions, MastodonApiActions} from "../../state/store/actions";
|
||||
import {debounceTime, Observable, Subscription, tap} from "rxjs";
|
||||
import {List} from 'projects/mastodon-api/src/public-api';
|
||||
import {selectFilters, selectFiltersForForm, selectLists} from "../../state/store/selectors";
|
||||
import {Filters} from "../../state/store/reducers";
|
||||
import {fromFilters, selectFiltersForForm, selectLists} from "../../state/store";
|
||||
|
||||
interface FiltersForForm {
|
||||
fullText: string | undefined;
|
||||
@@ -43,11 +41,11 @@ export class FiltersComponent implements OnDestroy {
|
||||
.pipe(debounceTime(500))
|
||||
.subscribe(value => {
|
||||
if (value['lists'] && value['lists'].length > 0) {
|
||||
this.store.dispatch(FiltersActions.setLists({lists: value.lists}));
|
||||
this.store.dispatch(FiltersActions.setUnlisted({unlisted: false}));
|
||||
this.store.dispatch(fromFilters.setLists({lists: value.lists}));
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: false}));
|
||||
}
|
||||
if (!value['lists'] || value['lists'].length === 0) {
|
||||
this.store.dispatch(FiltersActions.setLists({lists: []}));
|
||||
this.store.dispatch(fromFilters.setLists({lists: []}));
|
||||
}
|
||||
if (value['text']) {
|
||||
const text = value['text'];
|
||||
@@ -55,18 +53,18 @@ export class FiltersComponent implements OnDestroy {
|
||||
const parts = text.split(/[\s,]+/);
|
||||
if (parts.some((part: string) => part.startsWith('@'))) {
|
||||
const username = parts.find((part: string) => part.startsWith('@'));
|
||||
this.store.dispatch(FiltersActions.setUsername({username: username.substring(1)}));
|
||||
this.store.dispatch(fromFilters.setUsername({username: username.substring(1)}));
|
||||
} else {
|
||||
this.store.dispatch(FiltersActions.setUsername({username: ''}));
|
||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||
}
|
||||
this.store.dispatch(FiltersActions.setFreeText({freeText: parts.filter((part: string) => !part.startsWith('@'))}));
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: parts.filter((part: string) => !part.startsWith('@'))}));
|
||||
} else {
|
||||
this.store.dispatch(FiltersActions.setUsername({username: ''}));
|
||||
this.store.dispatch(FiltersActions.setFreeText({freeText: []}));
|
||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||
}
|
||||
} else {
|
||||
this.store.dispatch(FiltersActions.setUsername({username: ''}));
|
||||
this.store.dispatch(FiltersActions.setFreeText({freeText: []}));
|
||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -76,10 +74,11 @@ export class FiltersComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.store.dispatch(FiltersActions.clearFilters());
|
||||
this.store.dispatch(fromFilters.clearFilters());
|
||||
|
||||
}
|
||||
|
||||
toggleUnlisted($event: boolean) {
|
||||
this.store.dispatch(FiltersActions.setUnlisted({unlisted: $event}));
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: $event}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {MastodonApiAuthenticationService} from "../../../../../mastodon-api/src/lib/services/mastodon-api-authentication.service";
|
||||
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from "@angular/router";
|
||||
import {map, Observable, tap} from "rxjs";
|
||||
import {PersistentStore} from "../state/persistent/persistent-store.service";
|
||||
import {Store} from "@ngrx/store";
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(private persistentStore: PersistentStore,
|
||||
private store: Store,
|
||||
private router: Router,
|
||||
private mastodonApiAuthService: MastodonApiAuthenticationService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
|
||||
const currentInstance = this.persistentStore.value.currentInstance;
|
||||
if (currentInstance && currentInstance.accessToken && currentInstance.accountId) return true;
|
||||
if (currentInstance && currentInstance.accessToken && !currentInstance.accountId) {
|
||||
return this.mastodonApiAuthService.verifyCredentials(currentInstance.instanceName, currentInstance.accessToken).pipe(
|
||||
map((accountId) => {
|
||||
this.persistentStore.set(
|
||||
'currentInstance',
|
||||
{...currentInstance, 'accountId': accountId}
|
||||
);
|
||||
return !!accountId;
|
||||
})
|
||||
);
|
||||
}
|
||||
this.router.navigate(['/auth']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {MastodonApiAccountsService} from "../../../../../mastodon-api/src/lib/services/mastodon-api-accounts.service";
|
||||
import {PersistentStore} from "../state/persistent/persistent-store.service";
|
||||
import {EMPTY, expand, map, Observable, reduce} from "rxjs";
|
||||
import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {EMPTY, expand, map, Observable, reduce, Subscription, switchMap, tap} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {Account, MastodonApiAccountsService} from 'projects/mastodon-api/src/public-api';
|
||||
import {selectDataForAuthorizedRequest} from "../state/store";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService {
|
||||
export class AccountService implements OnDestroy {
|
||||
private instanceName: string | undefined;
|
||||
private accessToken: string | undefined;
|
||||
private accountId: string | undefined;
|
||||
private storeSubscription: Subscription;
|
||||
|
||||
constructor(private mastodonApiAccountsService: MastodonApiAccountsService,
|
||||
private store: PersistentStore) {
|
||||
constructor(private mastodonApiAccountsService: MastodonApiAccountsService, private store: Store) {
|
||||
this.storeSubscription = this.store.pipe(select(selectDataForAuthorizedRequest)).subscribe(data => {
|
||||
this.instanceName = data?.instanceName;
|
||||
this.accessToken = data?.accessToken;
|
||||
this.accountId = data?.accountId;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.storeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
loadFollowings(): Observable<ReadonlyArray<Account>> {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
const accountId = applicationState.currentInstance?.accountId;
|
||||
return this.mastodonApiAccountsService
|
||||
.getFollowingsForAccount(instanceName, accessToken!, accountId)
|
||||
.getFollowingsForAccount(this.instanceName!, this.accessToken!, this.accountId!)
|
||||
.pipe(
|
||||
expand(result => {
|
||||
const nextLink = result[0];
|
||||
if (nextLink && nextLink.length > 0) {
|
||||
return this.mastodonApiAccountsService.getFollowingsForAccount(instanceName, accessToken!, accountId, nextLink);
|
||||
return this.mastodonApiAccountsService
|
||||
.getFollowingsForAccount(this.instanceName!, this.accessToken!, this.accountId!, nextLink);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
|
||||
@@ -1,63 +1,47 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {EMPTY, expand, map, Observable, reduce, take} from "rxjs";
|
||||
import {MastodonApiListsService} from "../../../../../mastodon-api/src/lib/services/mastodon-api-lists.service";
|
||||
import {PersistentStore} from "../state/persistent/persistent-store.service";
|
||||
import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
import {List} from "../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {EMPTY, expand, filter, map, Observable, of, reduce, Subscription, switchMap, tap} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {List, MastodonApiListsService} from 'projects/mastodon-api/src/public-api';
|
||||
import {selectDataForAuthorizedRequest} from "../state/store";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ListService {
|
||||
export class ListService implements OnDestroy {
|
||||
|
||||
constructor(private mastodonApiListsService: MastodonApiListsService,
|
||||
private store: PersistentStore) {
|
||||
private instanceName: string | undefined;
|
||||
private accessToken: string | undefined;
|
||||
private storeSubscription: Subscription;
|
||||
|
||||
constructor(private mastodonApiListsService: MastodonApiListsService, private store: Store) {
|
||||
this.storeSubscription = this.store.pipe(select(selectDataForAuthorizedRequest)).subscribe(data => {
|
||||
this.instanceName = data?.instanceName;
|
||||
this.accessToken = data?.accessToken;
|
||||
});
|
||||
}
|
||||
|
||||
loadLists(): Observable<List[]> {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.getLists(instanceName, accessToken!)
|
||||
.pipe(map(result => result.sort((a, b) => a.title.localeCompare(b.title))));
|
||||
return this.mastodonApiListsService.getLists(this.instanceName!, this.accessToken!)
|
||||
}
|
||||
|
||||
|
||||
createList(title: string) {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.createList(instanceName, accessToken!, title)
|
||||
.pipe(
|
||||
take(1),
|
||||
);
|
||||
return this.mastodonApiListsService.createList(this.instanceName!, this.accessToken!, title);
|
||||
}
|
||||
|
||||
updateList(listId: string, newName: string) {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.updateList(instanceName, accessToken!, listId, newName)
|
||||
.pipe(
|
||||
take(1),
|
||||
);
|
||||
return this.mastodonApiListsService.updateList(this.instanceName!, this.accessToken!, listId, newName);
|
||||
}
|
||||
|
||||
|
||||
loadAccountsIdsForList(listId: string): Observable<{ [id: string]: string[] }> {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.getAccountsForList(instanceName, accessToken!, listId)
|
||||
.getAccountsForList(this.instanceName!, this.accessToken!, listId)
|
||||
.pipe(
|
||||
expand(result => {
|
||||
const nextLink = result[0];
|
||||
if (nextLink && nextLink.length > 0) {
|
||||
return this.mastodonApiListsService.getAccountsForList(instanceName, accessToken!, listId, nextLink);
|
||||
return this.mastodonApiListsService.getAccountsForList(this.instanceName!, this.accessToken!, listId, nextLink);
|
||||
}
|
||||
return EMPTY;
|
||||
}),
|
||||
@@ -78,25 +62,15 @@ export class ListService {
|
||||
}
|
||||
|
||||
addAccountToSelectedList(accountId: string, listId: string) {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.addAccountToList(instanceName, accessToken!, listId, accountId)
|
||||
.pipe(
|
||||
take(1),
|
||||
);
|
||||
return this.mastodonApiListsService.addAccountToList(this.instanceName!, this.accessToken!, listId, accountId)
|
||||
}
|
||||
|
||||
removeAccountFromList(accountId: string, listId: string) {
|
||||
const applicationState = this.store.value;
|
||||
const instanceName = applicationState.currentInstance?.instanceName;
|
||||
const accessToken = applicationState.currentInstance?.accessToken;
|
||||
return this.mastodonApiListsService
|
||||
.removeAccountFromList(instanceName, accessToken!, listId, accountId)
|
||||
.pipe(
|
||||
take(1),
|
||||
)
|
||||
return this.mastodonApiListsService.removeAccountFromList(this.instanceName!, this.accessToken!, listId, accountId)
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.storeSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
|
||||
function getLocalStorage(): Storage {
|
||||
return localStorage;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: "root"})
|
||||
export class LocalStorageRefService {
|
||||
get localStorage(): Storage {
|
||||
return getLocalStorage();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import {BehaviorSubject, distinctUntilChanged, map, Observable} from "rxjs";
|
||||
import {Injectable} from "@angular/core";
|
||||
import {LocalStorageRefService} from "./local-storage-ref.service";
|
||||
import {Instance, State} from "./state";
|
||||
|
||||
const state = <State>{};
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class PersistentStore {
|
||||
private _localStorage: Storage;
|
||||
private subject = new BehaviorSubject<State>(state);
|
||||
private store = this.subject.asObservable().pipe(distinctUntilChanged());
|
||||
|
||||
constructor(private _localStorageRefService: LocalStorageRefService) {
|
||||
this._localStorage = _localStorageRefService.localStorage;
|
||||
this.loadState();
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.subject.value;
|
||||
}
|
||||
|
||||
isAuthorized(): boolean {
|
||||
const currentInstance = this.value.currentInstance;
|
||||
if (!currentInstance) return false;
|
||||
if (!currentInstance.accessToken) return false;
|
||||
if (!currentInstance.accountId) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
select<T>(name: string): Observable<T> {
|
||||
return this.store.pipe(map(value => value ? <T>value[name as keyof State] : <T>{}));
|
||||
}
|
||||
|
||||
set(name: string, state: any) {
|
||||
this.subject.next({
|
||||
...this.value, [name as keyof State]: state
|
||||
});
|
||||
this.persistState();
|
||||
}
|
||||
|
||||
private persistState() {
|
||||
const jsonData = JSON.stringify(this.subject.value, this.stringifyMap);
|
||||
this._localStorage.setItem('applicationData', jsonData);
|
||||
}
|
||||
|
||||
private loadState() {
|
||||
const jsonData = this._localStorage.getItem('applicationData');
|
||||
if (jsonData) {
|
||||
const data = JSON.parse(jsonData, this.parseMap);
|
||||
this.subject.next(data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private stringifyMap(key: string, value: Map<string, Instance> | Map<string, string> | object) {
|
||||
if (value instanceof Map) {
|
||||
return {
|
||||
dataType: 'Map',
|
||||
value: [...value]
|
||||
};
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private parseMap(key: string, value: any) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (value.dataType === 'Map') {
|
||||
return new Map(value.value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {Account} from "../../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
import {List} from "../../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
|
||||
export interface Instance {
|
||||
instanceName: string;
|
||||
accessToken?: string;
|
||||
accountId: string;
|
||||
id: string;
|
||||
appName: string;
|
||||
website: string;
|
||||
redirectUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
vapidKey: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
currentInstance: Instance;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {createAction, createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||
import {List} from "../../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
import {Account} from "../../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
|
||||
export const MastodonApiActions = createActionGroup({
|
||||
source: 'Mastodon API',
|
||||
events: {
|
||||
'Load Lists': emptyProps(),
|
||||
'Load Followings': emptyProps(),
|
||||
'Load Accounts For List': emptyProps(),
|
||||
'Lists Loaded': props<{ lists: ReadonlyArray<List> }>(),
|
||||
'Lists Loaded Error': emptyProps(),
|
||||
'Account Ids for Lists Loaded': props<{ mappings: { [listId: string]: string[] } }>(),
|
||||
'Followings Loaded': props<{ followings: ReadonlyArray<Account> }>(),
|
||||
'Followings Loaded Error': props<{ error: string }>(),
|
||||
}
|
||||
});
|
||||
|
||||
export const ListActions = createActionGroup({
|
||||
source: 'List',
|
||||
events: {
|
||||
'Add Account to List': props<{ accountId: string, listId: string }>(),
|
||||
'Add Account to List Success': emptyProps(),
|
||||
'Add Account to List Error': emptyProps(),
|
||||
'Remove Account from List': props<{ accountId: string, listId: string }>(),
|
||||
'Remove Account from List Success': emptyProps(),
|
||||
'Remove Account from List Error': emptyProps(),
|
||||
'Create List': props<{ title: string }>(),
|
||||
'Create List Success': props<{ listId: string, title: string }>(),
|
||||
'Create List Error': emptyProps(),
|
||||
'Update List': props<{ listId: string, newTitle: string }>(),
|
||||
'Update List Success': props<{ listId: string, newTitle: string }>(),
|
||||
'Update List Error': emptyProps(),
|
||||
},
|
||||
});
|
||||
|
||||
export const FiltersActions = createActionGroup({
|
||||
source: 'Filters',
|
||||
events: {
|
||||
'Set Username': props<{ username: string }>(),
|
||||
'Set Free Text': props<{ freeText: ReadonlyArray<string> }>(),
|
||||
'Set Lists': props<{ lists: ReadonlyArray<string> }>(),
|
||||
'Set Unlisted': props<{ unlisted: boolean }>(),
|
||||
'Clear Filters': emptyProps(),
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import {createActionGroup, emptyProps} from "@ngrx/store";
|
||||
|
||||
export const fromApplication = createActionGroup({
|
||||
source: 'Application',
|
||||
events: {
|
||||
'Load Lists': emptyProps(),
|
||||
'Load Followings': emptyProps(),
|
||||
'Load Accounts For List': emptyProps(),
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||
|
||||
export const fromAuthorize = createActionGroup({
|
||||
source: 'Authorize',
|
||||
events: {
|
||||
'Load Local Storage': emptyProps(),
|
||||
'Register Application': props<{ instanceName: string, redirectUrl: string }>(),
|
||||
'Register Application Success': props<{ id: string, appName: string, website: string, clientId: string, clientSecret: string }>(),
|
||||
'Register Application Error': props<{ error: any }>(),
|
||||
'Authorize User': props<{ instanceName: string, clientId: string, redirectUrl: string }>(),
|
||||
'Get Access Token': props<{ code: string, instanceName: string, clientId: string, clientSecret: string, redirectUrl: string }>(),
|
||||
'Get Access Token Success': props<{ accessToken: string, accountId: string }>(),
|
||||
'Get Access Token Error': emptyProps(),
|
||||
'Logout': emptyProps(),
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||
|
||||
export const fromFilters = createActionGroup({
|
||||
source: 'Filters',
|
||||
events: {
|
||||
'Set Username': props<{ username: string }>(),
|
||||
'Set Free Text': props<{ freeText: ReadonlyArray<string> }>(),
|
||||
'Set Lists': props<{ lists: ReadonlyArray<string> }>(),
|
||||
'Set Unlisted': props<{ unlisted: boolean }>(),
|
||||
'Clear Filters': emptyProps(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./authorize.actions";
|
||||
export * from "./application.actions";
|
||||
export * from "./filters.actions";
|
||||
export * from './lists.actions';
|
||||
export * from './list-view.actions';
|
||||
export * from "./mastodon-api.actions";
|
||||
export * from './matrix-view.actions';
|
||||
export * from './sync.actions'
|
||||
export * from './table-view.actions';
|
||||
@@ -0,0 +1,10 @@
|
||||
import {createActionGroup, props} from "@ngrx/store";
|
||||
|
||||
export const fromListViewPage = createActionGroup({
|
||||
source: 'ListViewPage',
|
||||
events: {
|
||||
'Add Account to List': props<{ accountId: string, listId: string }>(),
|
||||
'Remove Account from List': props<{ accountId: string, listId: string }>(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createActionGroup, props} from "@ngrx/store";
|
||||
|
||||
export const fromListsPage = createActionGroup({
|
||||
source: 'ListsPage',
|
||||
events: {
|
||||
'Create List': props<{ title: string }>(),
|
||||
'Update List': props<{ listId: string, newTitle: string }>(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||
import {Account, List} from "projects/mastodon-api/src/public-api";
|
||||
|
||||
export const fromMastodonApi = createActionGroup({
|
||||
source: 'MastodonApi',
|
||||
events: {
|
||||
'Lists Loaded Success': props<{ lists: ReadonlyArray<List> }>(),
|
||||
'Lists Loaded Error': emptyProps(),
|
||||
|
||||
'Account Ids for Lists Loaded Success': props<{ mappings: { [listId: string]: string[] } }>(),
|
||||
|
||||
'Followings Loaded Success': props<{ followings: ReadonlyArray<Account> }>(),
|
||||
'Followings Loaded Error': props<{ error: string }>(),
|
||||
|
||||
'Add Account to List Success': emptyProps(),
|
||||
'Add Account to List Error': emptyProps(),
|
||||
|
||||
'Remove Account from List Success': emptyProps(),
|
||||
'Remove Account from List Error': emptyProps(),
|
||||
|
||||
'Create List Success': props<{ listId: string, title: string }>(),
|
||||
'Create List Error': emptyProps(),
|
||||
|
||||
'Update List Success': props<{ listId: string, newTitle: string }>(),
|
||||
'Update List Error': emptyProps(),
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createActionGroup, props} from "@ngrx/store";
|
||||
|
||||
export const fromMatrixViewPage = createActionGroup({
|
||||
source: 'MatrixViewPage',
|
||||
events: {
|
||||
'Add Account to List': props<{ accountId: string, listId: string }>(),
|
||||
'Remove Account from List': props<{ accountId: string, listId: string }>(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import {createActionGroup, emptyProps} from "@ngrx/store";
|
||||
|
||||
export const fromSyncPage = createActionGroup({
|
||||
source: 'SyncPage',
|
||||
events: {
|
||||
'Load Lists': emptyProps(),
|
||||
'Load Followings': emptyProps(),
|
||||
'Load Accounts For List': emptyProps(),
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createActionGroup, props} from "@ngrx/store";
|
||||
|
||||
export const fromTableViewPage = createActionGroup({
|
||||
source: 'TableViewPage',
|
||||
events: {
|
||||
'Add Account to List': props<{ accountId: string, listId: string }>(),
|
||||
'Remove Account from List': props<{ accountId: string, listId: string }>(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import {List} from "../../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||
import {Account} from "../../../../../../mastodon-api/src/lib/interfaces/public/account";
|
||||
|
||||
export interface Filters {
|
||||
username: string;
|
||||
freeText: ReadonlyArray<string>;
|
||||
lists: ReadonlyArray<string>;
|
||||
unlisted: boolean;
|
||||
}
|
||||
|
||||
export interface ApplicationState {
|
||||
listsLoading: boolean;
|
||||
listAccountsLoading: boolean;
|
||||
followingsLoading: boolean;
|
||||
lists: ReadonlyArray<List>;
|
||||
listsAccounts: { [listId: string]: string[] };
|
||||
followings: ReadonlyArray<Account>;
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
export const initialState: ApplicationState = {
|
||||
listsLoading: false,
|
||||
listAccountsLoading: false,
|
||||
followingsLoading: false,
|
||||
lists: [],
|
||||
listsAccounts: {},
|
||||
followings: [],
|
||||
filters: {
|
||||
username: '',
|
||||
freeText: [],
|
||||
lists: [],
|
||||
unlisted: false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface AuthenticationState {
|
||||
registeringApplication: boolean;
|
||||
applicationRegistered: boolean;
|
||||
authorizingUser: boolean;
|
||||
instanceName?: string;
|
||||
accessToken?: string;
|
||||
accountId?: string;
|
||||
id?: string;
|
||||
appName?: string;
|
||||
website?: string;
|
||||
redirectUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
vapidKey?: string;
|
||||
}
|
||||
|
||||
export const initialState: AuthenticationState = {
|
||||
registeringApplication: false,
|
||||
applicationRegistered: false,
|
||||
authorizingUser: false,
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {act, Actions, createEffect, ofType} from "@ngrx/effects";
|
||||
import {ListActions, MastodonApiActions} from "./actions";
|
||||
import {catchError, concat, exhaustMap, forkJoin, map, merge, of, switchMap, tap} from "rxjs";
|
||||
import {ListService} from "../../services/list.service";
|
||||
import {AccountService} from "../../services/account.service";
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {List} from "projects/mastodon-api/src/public-api";
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationEffects {
|
||||
loadLists$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(MastodonApiActions.loadLists),
|
||||
exhaustMap(() => this.listService.loadLists().pipe(
|
||||
map(lists => MastodonApiActions.listsLoaded({lists})),
|
||||
catchError(() => of(MastodonApiActions.listsLoadedError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
updateList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.updateList),
|
||||
exhaustMap((action) => this.listService.updateList(action.listId, action.newTitle).pipe(
|
||||
map((resp) => {
|
||||
const list = resp as List;
|
||||
return ListActions.updateListSuccess({listId: list.id, newTitle: list.title});
|
||||
}),
|
||||
catchError(() => of(ListActions.updateListError)),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
updateListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.updateListSuccess),
|
||||
map(() => this.toastService.success('List updated successfully', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
updateListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.updateListError),
|
||||
map(() => this.toastService.success('Could not update the list', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
createList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.createList),
|
||||
exhaustMap((action) => this.listService.createList(action.title).pipe(
|
||||
map((resp) => {
|
||||
const list = resp as List;
|
||||
return ListActions.createListSuccess({listId: list.id, title: list.title});
|
||||
}),
|
||||
catchError(() => of(ListActions.createListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
createListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.createListSuccess),
|
||||
map(() => this.toastService.success('List created successfully', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
createListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.createListError),
|
||||
map(() => this.toastService.success('Could not create the list', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
|
||||
listsLoaded$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(MastodonApiActions.listsLoaded),
|
||||
switchMap((action) => {
|
||||
return forkJoin(action.lists.map(list => this.listService.loadAccountsIdsForList(list.id)))
|
||||
.pipe(
|
||||
map(mappings => {
|
||||
const accountsInList: { [id: string]: string[] } = mappings.reduce((acc, mapping) => {
|
||||
return {...acc, ...mapping};
|
||||
}, {});
|
||||
return MastodonApiActions.accountIdsForListsLoaded({mappings: accountsInList})
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadFollowings$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(MastodonApiActions.loadFollowings),
|
||||
exhaustMap(() => this.accountService.loadFollowings().pipe(
|
||||
map(followings => MastodonApiActions.followingsLoaded({followings})),
|
||||
catchError((error) => of(MastodonApiActions.followingsLoadedError({error}))),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
addAccountToList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.addAccountToList),
|
||||
exhaustMap((action) => this.listService.addAccountToSelectedList(action.accountId, action.listId).pipe(
|
||||
map(() => ListActions.addAccountToListSuccess()),
|
||||
catchError(() => of(ListActions.addAccountToListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
addAccountToListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.addAccountToListSuccess),
|
||||
map(() => this.toastService.success('Account added to list', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
addCountsToListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.addAccountToListError),
|
||||
map(() => this.toastService.danger('Could not add account to list, please reload', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
removeAccountFromList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.removeAccountFromList),
|
||||
exhaustMap((action) => this.listService.removeAccountFromList(action.accountId, action.listId).pipe(
|
||||
map(() => ListActions.removeAccountFromListSuccess()),
|
||||
catchError(() => of(ListActions.removeAccountFromListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
removeAccountFromListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.removeAccountFromListSuccess),
|
||||
map(() => this.toastService.success('Account removed from list', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
removeAccountFromListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ListActions.removeAccountFromListError),
|
||||
map(() => this.toastService.danger('Could not remove account from list, please reload', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private listService: ListService,
|
||||
private accountService: AccountService,
|
||||
private toastService: NbToastrService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||
import {catchError, exhaustMap, map, of, switchMap, tap, zip} from "rxjs";
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {List} from "projects/mastodon-api/src/public-api";
|
||||
import {fromApplication, fromListsPage, fromListViewPage, fromMastodonApi, fromMatrixViewPage, fromSyncPage, fromTableViewPage} from "../actions";
|
||||
import {ListService} from "../../../services/list.service";
|
||||
import {AccountService} from "../../../services/account.service";
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationStateEffects {
|
||||
loadLists$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromApplication.loadLists, fromSyncPage.loadLists),
|
||||
exhaustMap(() => this.listService.loadLists().pipe(
|
||||
map(lists => fromMastodonApi.listsLoadedSuccess({lists})),
|
||||
catchError(() => of(fromMastodonApi.listsLoadedError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
updateList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromListsPage.updateList),
|
||||
exhaustMap((action) => this.listService.updateList(action.listId, action.newTitle).pipe(
|
||||
map((resp) => {
|
||||
const list = resp as List;
|
||||
return fromMastodonApi.updateListSuccess({listId: list.id, newTitle: list.title});
|
||||
}),
|
||||
catchError(() => of(fromMastodonApi.updateListError)),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
updateListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.updateListSuccess),
|
||||
map(() => this.toastService.success('List updated successfully', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
updateListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.updateListError),
|
||||
map(() => this.toastService.danger('Could not update the list', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
createList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromListsPage.createList),
|
||||
exhaustMap((action) => this.listService.createList(action.title).pipe(
|
||||
map((resp) => {
|
||||
const list = resp as List;
|
||||
return fromMastodonApi.createListSuccess({listId: list.id, title: list.title});
|
||||
}),
|
||||
catchError(() => of(fromMastodonApi.createListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
createListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.createListSuccess),
|
||||
map(() => this.toastService.success('List created successfully', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
createListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.createListError),
|
||||
map(() => this.toastService.danger('Could not create the list', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
listsLoaded$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.listsLoadedSuccess),
|
||||
switchMap((action) => {
|
||||
this.toastService.success('Lists loaded successfully', 'Success');
|
||||
if (action.lists.length === 0) return of(fromMastodonApi.accountIdsForListsLoadedSuccess({mappings: {}}));
|
||||
const mappingsArr = action.lists.map((list) => this.listService.loadAccountsIdsForList(list.id));
|
||||
return zip(mappingsArr)
|
||||
.pipe(
|
||||
map(mappings => {
|
||||
const accountsInList: { [id: string]: string[] } = mappings.reduce((acc, mapping) => {
|
||||
return {...acc, ...mapping};
|
||||
}, {});
|
||||
return fromMastodonApi.accountIdsForListsLoadedSuccess({mappings: accountsInList})
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadFollowings$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromApplication.loadFollowings, fromSyncPage.loadFollowings),
|
||||
switchMap(() => this.accountService.loadFollowings().pipe(
|
||||
map(followings => {
|
||||
return fromMastodonApi.followingsLoadedSuccess({followings});
|
||||
}),
|
||||
catchError((error) => of(fromMastodonApi.followingsLoadedError({error}))),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadFollowingsSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.followingsLoadedSuccess),
|
||||
tap(() => this.toastService.success('Followings loaded successfully', 'Success')),
|
||||
)
|
||||
, {dispatch: false});
|
||||
|
||||
loadFollowingsError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.followingsLoadedError),
|
||||
tap(() => this.toastService.danger('Followings could not be loaded', 'Error')),
|
||||
)
|
||||
, {dispatch: false});
|
||||
|
||||
addAccountToList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromListViewPage.addAccountToList, fromMatrixViewPage.addAccountToList, fromTableViewPage.addAccountToList),
|
||||
exhaustMap((action) => this.listService.addAccountToSelectedList(action.accountId, action.listId).pipe(
|
||||
map(() => fromMastodonApi.addAccountToListSuccess()),
|
||||
catchError(() => of(fromMastodonApi.addAccountToListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
addAccountToListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.addAccountToListSuccess),
|
||||
map(() => this.toastService.success('Account added to list', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
addCountsToListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.addAccountToListError),
|
||||
map(() => this.toastService.danger('Could not add account to list, please reload', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
removeAccountFromList$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromListViewPage.removeAccountFromList, fromMatrixViewPage.removeAccountFromList, fromTableViewPage.removeAccountFromList),
|
||||
exhaustMap((action) => this.listService.removeAccountFromList(action.accountId, action.listId).pipe(
|
||||
map(() => fromMastodonApi.removeAccountFromListSuccess()),
|
||||
catchError(() => of(fromMastodonApi.removeAccountFromListError())),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
removeAccountFromListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.removeAccountFromListSuccess),
|
||||
map(() => this.toastService.success('Account removed from list', 'Success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
removeAccountFromListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.removeAccountFromListError),
|
||||
map(() => this.toastService.danger('Could not remove account from list, please reload', 'Error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private listService: ListService,
|
||||
private accountService: AccountService,
|
||||
private toastService: NbToastrService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||
import {fromAuthorize} from "../actions";
|
||||
import {catchError, exhaustMap, map, of, switchMap, tap, throwError} from "rxjs";
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {MastodonApiAuthenticationService, RegisteredApp} from "projects/mastodon-api/src/public-api";
|
||||
import {Store} from "@ngrx/store";
|
||||
import {environment} from "../../../../../environments/environment";
|
||||
|
||||
@Injectable()
|
||||
export class AuthorizationStateEffects {
|
||||
registerApplication$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromAuthorize.registerApplication),
|
||||
exhaustMap((action) =>
|
||||
this.mastodonApiAuthService
|
||||
.createApp(action.instanceName, environment.appName, action.redirectUrl, environment.appWebsite)
|
||||
.pipe(
|
||||
map((registeredApp: RegisteredApp) => {
|
||||
this.toastService.success('Successfully registered the application with the instance', 'Success');
|
||||
return fromAuthorize.registerApplicationSuccess({
|
||||
id: registeredApp.id,
|
||||
appName: registeredApp.name,
|
||||
website: registeredApp.website,
|
||||
clientId: registeredApp.clientId,
|
||||
clientSecret: registeredApp.clientSecret,
|
||||
})
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toastService.danger('Could not register application. Please check the instance name.', 'Error');
|
||||
return of(fromAuthorize.registerApplicationError({error}));
|
||||
}),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
getAccessToken$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromAuthorize.getAccessToken),
|
||||
switchMap((action) =>
|
||||
this.mastodonApiAuthService
|
||||
.getAccessToken(action.instanceName!, action.clientId!, action.clientSecret!, action.redirectUrl!, action.code)
|
||||
.pipe(
|
||||
map(accessToken => {
|
||||
return {action, accessToken};
|
||||
})
|
||||
)
|
||||
),
|
||||
switchMap((result) =>
|
||||
this.mastodonApiAuthService
|
||||
.verifyCredentials(result.action.instanceName, result.accessToken)
|
||||
.pipe(
|
||||
map((accountId) => {
|
||||
return {accessToken: result.accessToken, accountId};
|
||||
})
|
||||
)
|
||||
),
|
||||
map((result) => {
|
||||
this.toastService.success('User successfully authenticated', 'Success');
|
||||
return fromAuthorize.getAccessTokenSuccess({accessToken: result.accessToken, accountId: result.accountId});
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
this.toastService.danger('Could not get access token', 'Error');
|
||||
return of(fromAuthorize.getAccessTokenError());
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
authorizeUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromAuthorize.authorizeUser),
|
||||
map((action) => {
|
||||
this.mastodonApiAuthService.authorizeUser(action.instanceName!, action.clientId!, action.redirectUrl!);
|
||||
})
|
||||
), {dispatch: false});
|
||||
|
||||
constructor(
|
||||
private store: Store,
|
||||
private actions$: Actions,
|
||||
private mastodonApiAuthService: MastodonApiAuthenticationService,
|
||||
private toastService: NbToastrService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './application-state.effects';
|
||||
export * from './authorization-state.effects';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./actions/index";
|
||||
export * from "./selectors";
|
||||
export * from "./effects";
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import {createReducer, on} from "@ngrx/store";
|
||||
import {Account, List} from "projects/mastodon-api/src/public-api";
|
||||
import {FiltersActions, ListActions, MastodonApiActions} from "./actions";
|
||||
|
||||
export interface Filters {
|
||||
username: string;
|
||||
freeText: ReadonlyArray<string>;
|
||||
lists: ReadonlyArray<string>;
|
||||
unlisted: boolean;
|
||||
}
|
||||
|
||||
export interface ApplicationState {
|
||||
listsLoading: boolean;
|
||||
listAccountsLoading: boolean;
|
||||
followingsLoading: boolean;
|
||||
lists: ReadonlyArray<List>;
|
||||
listsAccounts: { [listId: string]: string[] };
|
||||
followings: ReadonlyArray<Account>;
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
export const initialState: ApplicationState = {
|
||||
listsLoading: false,
|
||||
listAccountsLoading: false,
|
||||
followingsLoading: false,
|
||||
lists: [],
|
||||
listsAccounts: {},
|
||||
followings: [],
|
||||
filters: {
|
||||
username: '',
|
||||
freeText: [],
|
||||
lists: [],
|
||||
unlisted: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const applicationStateReducers = createReducer(
|
||||
initialState,
|
||||
on(MastodonApiActions.loadLists, _state => {
|
||||
return {..._state, listsLoading: true};
|
||||
}),
|
||||
on(MastodonApiActions.listsLoaded, (_state, {lists}) => {
|
||||
return {..._state, listsLoading: false, listAccountsLoading: true, lists: lists,};
|
||||
}),
|
||||
on(MastodonApiActions.accountIdsForListsLoaded, (_state, {mappings}) => {
|
||||
return {..._state, listAccountsLoading: false, listsAccounts: mappings};
|
||||
}),
|
||||
on(MastodonApiActions.followingsLoaded, (_state, {followings}) => {
|
||||
return {..._state, followingsLoading: false, followings};
|
||||
}),
|
||||
on(MastodonApiActions.followingsLoadedError, (_state, {error}) => {
|
||||
console.error(error);
|
||||
return {..._state};
|
||||
}),
|
||||
on(ListActions.addAccountToList, (_state, {accountId, listId}) => {
|
||||
const existingAccountIds = _state.listsAccounts[listId] || [];
|
||||
const newAccountIds = [...existingAccountIds, accountId];
|
||||
const newMap: { [listId: string]: string[] } = {..._state.listsAccounts, [listId]: newAccountIds};
|
||||
return <ApplicationState>{
|
||||
..._state,
|
||||
listsAccounts: newMap,
|
||||
}
|
||||
}),
|
||||
on(ListActions.removeAccountFromList, (_state, {accountId, listId}) => {
|
||||
const existingAccountIds = _state.listsAccounts[listId] || [];
|
||||
const newAccountIds = existingAccountIds.filter(id => id !== accountId);
|
||||
const newMap: { [listId: string]: string[] } = {..._state.listsAccounts, [listId]: newAccountIds};
|
||||
return <ApplicationState>{
|
||||
..._state,
|
||||
listsAccounts: newMap,
|
||||
}
|
||||
}),
|
||||
on(FiltersActions.setUsername, (_state, {username}) => {
|
||||
return {..._state, filters: {..._state.filters, username}};
|
||||
}),
|
||||
on(FiltersActions.setFreeText, (_state, {freeText}) => {
|
||||
return {..._state, filters: {..._state.filters, freeText}};
|
||||
}),
|
||||
on(FiltersActions.setLists, (_state, {lists}) => {
|
||||
return {..._state, filters: {..._state.filters, lists}};
|
||||
}),
|
||||
on(FiltersActions.setUnlisted, (_state, {unlisted}) => {
|
||||
let newState = {..._state, filters: {..._state.filters, unlisted}};
|
||||
if (unlisted) {
|
||||
newState = {...newState, filters: {...newState.filters, lists: []}};
|
||||
}
|
||||
return newState;
|
||||
}),
|
||||
on(FiltersActions.clearFilters, _state => {
|
||||
return {..._state, filters: initialState.filters};
|
||||
}),
|
||||
on(ListActions.createListSuccess, (_state, {listId, title}) => {
|
||||
const newListsArray = [..._state.lists, {id: listId, title: title, repliesPolicy: 'list', accounts: []}].sort((a, b) => a.title.localeCompare(b.title));
|
||||
return {
|
||||
..._state,
|
||||
listsAccounts: {..._state.listsAccounts, [listId]: []},
|
||||
lists: newListsArray,
|
||||
};
|
||||
}),
|
||||
on(ListActions.updateListSuccess, (_state, {listId, newTitle}) => {
|
||||
const listToUpdate = _state.lists.find(list => list.id === listId) as List;
|
||||
const newListsArray = [..._state.lists.filter(list => list.id !== listId), {...listToUpdate, title: newTitle}].sort((a, b) => a.title.localeCompare(b.title));
|
||||
return {
|
||||
..._state,
|
||||
lists: newListsArray,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,95 @@
|
||||
import {createReducer, on} from "@ngrx/store";
|
||||
import {List} from "projects/mastodon-api/src/public-api";
|
||||
import {fromApplication, fromFilters, fromListViewPage, fromMastodonApi, fromMatrixViewPage, fromTableViewPage} from "../actions";
|
||||
import {ApplicationState, initialState} from "../application-state";
|
||||
|
||||
export const applicationStateReducer = createReducer(
|
||||
initialState,
|
||||
on(fromApplication.loadLists, _state => {
|
||||
return {..._state, listsLoading: true};
|
||||
}),
|
||||
on(fromMastodonApi.listsLoadedSuccess, (_state, {lists}) => {
|
||||
return {..._state, listsLoading: false, listAccountsLoading: true, lists: lists,};
|
||||
}),
|
||||
on(fromMastodonApi.accountIdsForListsLoadedSuccess, (_state, {mappings}) => {
|
||||
return {..._state, listAccountsLoading: false, listsAccounts: mappings};
|
||||
}),
|
||||
on(fromMastodonApi.followingsLoadedSuccess, (_state, {followings}) => {
|
||||
return {..._state, followingsLoading: false, followings};
|
||||
}),
|
||||
on(fromMastodonApi.followingsLoadedError, (_state, {error}) => {
|
||||
console.error(error);
|
||||
return {..._state};
|
||||
}),
|
||||
on(
|
||||
fromListViewPage.addAccountToList,
|
||||
fromMatrixViewPage.addAccountToList,
|
||||
fromTableViewPage.addAccountToList,
|
||||
(_state, {accountId, listId}) => {
|
||||
const existingAccountIds = _state.listsAccounts[listId] || [];
|
||||
const newAccountIds = [...existingAccountIds, accountId];
|
||||
const newMap: { [listId: string]: string[] } = {..._state.listsAccounts, [listId]: newAccountIds};
|
||||
return <ApplicationState>{
|
||||
..._state,
|
||||
listsAccounts: newMap,
|
||||
}
|
||||
}),
|
||||
on(
|
||||
fromListViewPage.removeAccountFromList,
|
||||
fromMatrixViewPage.removeAccountFromList,
|
||||
fromTableViewPage.removeAccountFromList,
|
||||
(_state, {accountId, listId}) => {
|
||||
const existingAccountIds = _state.listsAccounts[listId] || [];
|
||||
const newAccountIds = existingAccountIds.filter(id => id !== accountId);
|
||||
const newMap: { [listId: string]: string[] } = {..._state.listsAccounts, [listId]: newAccountIds};
|
||||
return <ApplicationState>{
|
||||
..._state,
|
||||
listsAccounts: newMap,
|
||||
}
|
||||
}),
|
||||
on(
|
||||
fromFilters.setUsername,
|
||||
(_state, {username}) => {
|
||||
return {..._state, filters: {..._state.filters, username}};
|
||||
}),
|
||||
on(
|
||||
fromFilters.setFreeText,
|
||||
(_state, {freeText}) => {
|
||||
return {..._state, filters: {..._state.filters, freeText}};
|
||||
}),
|
||||
on(
|
||||
fromFilters.setLists,
|
||||
(_state, {lists}) => {
|
||||
return {..._state, filters: {..._state.filters, lists}};
|
||||
}),
|
||||
on(
|
||||
fromFilters.setUnlisted,
|
||||
(_state, {unlisted}) => {
|
||||
let newState = {..._state, filters: {..._state.filters, unlisted}};
|
||||
if (unlisted) {
|
||||
newState = {...newState, filters: {...newState.filters, lists: []}};
|
||||
}
|
||||
return newState;
|
||||
}),
|
||||
on(
|
||||
fromFilters.clearFilters,
|
||||
_state => {
|
||||
return {..._state, filters: initialState.filters};
|
||||
}),
|
||||
on(fromMastodonApi.createListSuccess, (_state, {listId, title}) => {
|
||||
const newListsArray = [..._state.lists, {id: listId, title: title, repliesPolicy: 'list', accounts: []}].sort((a, b) => a.title.localeCompare(b.title));
|
||||
return {
|
||||
..._state,
|
||||
listsAccounts: {..._state.listsAccounts, [listId]: []},
|
||||
lists: newListsArray,
|
||||
};
|
||||
}),
|
||||
on(fromMastodonApi.updateListSuccess, (_state, {listId, newTitle}) => {
|
||||
const listToUpdate = _state.lists.find(list => list.id === listId) as List;
|
||||
const newListsArray = [..._state.lists.filter(list => list.id !== listId), {...listToUpdate, title: newTitle}].sort((a, b) => a.title.localeCompare(b.title));
|
||||
return {
|
||||
..._state,
|
||||
lists: newListsArray,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
import {createReducer, on} from "@ngrx/store";
|
||||
import {initialState} from "../authentication-state";
|
||||
import {fromAuthorize} from "../actions";
|
||||
|
||||
export const authenticationStateReducer = createReducer(
|
||||
initialState,
|
||||
on(fromAuthorize.registerApplication, (_state, {instanceName, redirectUrl}) => {
|
||||
return {
|
||||
..._state,
|
||||
registeringApplication: true,
|
||||
instanceName,
|
||||
redirectUrl,
|
||||
};
|
||||
}),
|
||||
on(fromAuthorize.registerApplicationSuccess, (_state, {id, appName, website, clientId, clientSecret}) => {
|
||||
return {
|
||||
..._state,
|
||||
registeringApplication: false,
|
||||
applicationRegistered: true,
|
||||
id,
|
||||
appName,
|
||||
website,
|
||||
clientId,
|
||||
clientSecret,
|
||||
};
|
||||
}),
|
||||
on(fromAuthorize.registerApplicationError, (_state) => {
|
||||
return {
|
||||
..._state,
|
||||
registeringApplication: false,
|
||||
applicationRegistered: false,
|
||||
};
|
||||
}),
|
||||
on(fromAuthorize.logout, (_state) => {
|
||||
return {
|
||||
...initialState,
|
||||
};
|
||||
}),
|
||||
on(fromAuthorize.authorizeUser, (_state) => {
|
||||
return {
|
||||
..._state,
|
||||
authorizingUser: true,
|
||||
}
|
||||
}),
|
||||
on(fromAuthorize.getAccessTokenSuccess, (_state, {accessToken, accountId}) => {
|
||||
return {
|
||||
..._state,
|
||||
accessToken,
|
||||
accountId,
|
||||
authorizingUser: false,
|
||||
};
|
||||
}),
|
||||
on(fromAuthorize.getAccessTokenError, (_state) => {
|
||||
return {
|
||||
..._state,
|
||||
authorizingUser: false,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './application-state.reducer';
|
||||
export * from './authentication-state.reducer';
|
||||
@@ -1,12 +1,11 @@
|
||||
import {ApplicationState} from "./reducers";
|
||||
import {createFeatureSelector, createSelector, State} from "@ngrx/store";
|
||||
import {createFeatureSelector, createSelector} from "@ngrx/store";
|
||||
import {ApplicationState} from "../application-state";
|
||||
|
||||
|
||||
export const applicationStateFeature = createFeatureSelector<ApplicationState>('applicationState');
|
||||
export const applicationStateFeature = createFeatureSelector<ApplicationState>('application');
|
||||
|
||||
export const selectLists = createSelector(
|
||||
applicationStateFeature,
|
||||
(state: ApplicationState) => state.lists
|
||||
(state: ApplicationState) => [...state.lists].sort((a, b) => a.title.localeCompare(b.title))
|
||||
)
|
||||
export const selectFollowings = createSelector(
|
||||
applicationStateFeature,
|
||||
@@ -24,7 +23,7 @@ export const selectListsWithAccounts = createSelector(
|
||||
selectMappings,
|
||||
(lists, followings, mappings) => {
|
||||
return lists.map(list => {
|
||||
const accountIds = mappings[list.id];
|
||||
const accountIds = mappings[list.id] || [];
|
||||
const accounts = followings.filter(account => accountIds.includes(account.id));
|
||||
return {...list, accounts};
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import {createFeatureSelector, createSelector} from "@ngrx/store";
|
||||
import {AuthenticationState} from "../authentication-state";
|
||||
|
||||
export const authenticationStateFeature = createFeatureSelector<AuthenticationState>('authentication');
|
||||
|
||||
export const selectIsLoggedIn = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.instanceName !== undefined && state.accessToken !== undefined && state.accountId !== undefined
|
||||
)
|
||||
|
||||
export const selectDataForAuthorizedRequest = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => {
|
||||
return {
|
||||
instanceName: state.instanceName,
|
||||
accessToken: state.accessToken,
|
||||
accountId: state.accountId,
|
||||
}
|
||||
}
|
||||
);
|
||||
export const selectDataForAuthorizationFlow = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => {
|
||||
return {
|
||||
instanceName: state.instanceName,
|
||||
clientId: state.clientId,
|
||||
clientSecret: state.clientSecret,
|
||||
redirectUrl: state.redirectUrl,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const selectCurrentInstance = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.instanceName
|
||||
)
|
||||
export const selectCurrentInstanceWithApplicationRegisteredState = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => {
|
||||
return {instanceName: state.instanceName, applicationRegistered: state.applicationRegistered};
|
||||
}
|
||||
)
|
||||
|
||||
export const selectRegisteringApplicationFlag = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.registeringApplication
|
||||
)
|
||||
export const selectApplicationRegisteredFlag = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.applicationRegistered
|
||||
)
|
||||
|
||||
export const selectAuthorizingUserFlag = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.authorizingUser
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './application-state.selectors';
|
||||
export * from './authentication-state.selectors';
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div style="display:flex; flex-direction: column;">
|
||||
<div style="display:flex; flex-direction: column;" *ngIf="(instanceName$ | async) as instanceName">
|
||||
<a [href]="'https://' + instanceName + '/settings/export'" rel="noreferrer" target="_blank">
|
||||
Export lists using your account settings page
|
||||
</a>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {MastodonApiActions} from "../../shared/state/store/actions";
|
||||
import {selectFollowings, selectLists, selectListsWithAccounts, selectLoading} from "../../shared/state/store/selectors";
|
||||
import {Observable, tap} from "rxjs";
|
||||
import {selectCurrentInstance, fromSyncPage, selectFollowings, selectListsWithAccounts, selectLoading} from "../../shared/state/store";
|
||||
import {Observable} from "rxjs";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-sync',
|
||||
@@ -15,18 +13,18 @@ export class SyncComponent {
|
||||
loading$: Observable<boolean>;
|
||||
followings$: Observable<ReadonlyArray<Account>>;
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
instanceName: string;
|
||||
instanceName$: Observable<string | undefined>;
|
||||
|
||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
||||
this.instanceName = persistentStore.value.currentInstance.instanceName;
|
||||
constructor(private store: Store) {
|
||||
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||
this.followings$ = this.store.pipe(select(selectFollowings))
|
||||
this.lists$ = this.store.pipe(select(selectListsWithAccounts));
|
||||
this.loading$ = this.store.pipe(select(selectLoading));
|
||||
}
|
||||
|
||||
loadListsAndAccounts() {
|
||||
this.store.dispatch(MastodonApiActions.loadLists());
|
||||
this.store.dispatch(MastodonApiActions.loadFollowings());
|
||||
this.store.dispatch(fromSyncPage.loadLists());
|
||||
this.store.dispatch(fromSyncPage.loadFollowings());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
6
projects/mastolists/src/environments/environment.prod.ts
Normal file
6
projects/mastolists/src/environments/environment.prod.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
defaultInstance: 'mastodon.social',
|
||||
appName: 'Mastolists',
|
||||
appWebsite: 'https://mastolists.novaloop.cloud',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
defaultInstance: 'novaloop.social',
|
||||
appName: 'Mastolists (staging)',
|
||||
appWebsite: 'https://mastolists-staging.novaloop.cloud',
|
||||
}
|
||||
6
projects/mastolists/src/environments/environment.ts
Normal file
6
projects/mastolists/src/environments/environment.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
defaultInstance: 'novaloop.social',
|
||||
appName: 'Masterlists (dev)',
|
||||
appWebsite: 'http://localhost:4200',
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../out-tsc/app",
|
||||
"types": []
|
||||
"types": [],
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
|
||||
Reference in New Issue
Block a user