feat: adds authentication to ngrx store
This commit is contained in:
32
angular.json
32
angular.json
@@ -36,7 +36,14 @@
|
|||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"budgets": [{
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "projects/mastolists/src/environments/environment.ts",
|
||||||
|
"with": "projects/mastolists/src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "2mb",
|
"maximumWarning": "2mb",
|
||||||
"maximumError": "3mb"
|
"maximumError": "3mb"
|
||||||
@@ -56,6 +63,27 @@
|
|||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"namedChunks": 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"
|
"defaultConfiguration": "production"
|
||||||
@@ -136,4 +164,4 @@
|
|||||||
"cli": {
|
"cli": {
|
||||||
"analytics": "48f19321-a0cb-4b6d-8ea6-27d1fd0792f5"
|
"analytics": "48f19321-a0cb-4b6d-8ea6-27d1fd0792f5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
150
package.json
150
package.json
@@ -1,79 +1,81 @@
|
|||||||
{
|
{
|
||||||
"name": "mastodon",
|
"name": "mastodon",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"build:prod": "ng build --configuration=production",
|
||||||
"test": "ng test"
|
"build:staging": "ng build --configuration=staging",
|
||||||
},
|
"watch": "ng build --watch --configuration development",
|
||||||
"private": true,
|
"test": "ng test"
|
||||||
"dependencies": {
|
|
||||||
"@angular/animations": "^15.0.0",
|
|
||||||
"@angular/common": "^15.0.0",
|
|
||||||
"@angular/compiler": "^15.0.0",
|
|
||||||
"@angular/core": "^15.0.0",
|
|
||||||
"@angular/forms": "^15.0.0",
|
|
||||||
"@angular/platform-browser": "^15.0.0",
|
|
||||||
"@angular/platform-browser-dynamic": "^15.0.0",
|
|
||||||
"@angular/router": "^15.0.0",
|
|
||||||
"@nebular/auth": "10.0.0",
|
|
||||||
"@nebular/eva-icons": "^10.0.0",
|
|
||||||
"@nebular/security": "10.0.0",
|
|
||||||
"@nebular/theme": "10.0.0",
|
|
||||||
"@ngrx/effects": "^15.0.0",
|
|
||||||
"@ngrx/entity": "^15.0.0",
|
|
||||||
"@ngrx/store": "^15.0.0",
|
|
||||||
"@ngrx/store-devtools": "^15.0.0",
|
|
||||||
"axios": "^1.2.1",
|
|
||||||
"eva-icons": "^1.1.3",
|
|
||||||
"ngx-progressbar": "^9.0.0",
|
|
||||||
"oauth": "^0.10.0",
|
|
||||||
"object-assign-deep": "^0.4.0",
|
|
||||||
"parse-link-header": "^2.0.0",
|
|
||||||
"rxjs": "~7.5.0",
|
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"zone.js": "~0.12.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@angular-builders/custom-webpack": "^15.0.0-beta.0",
|
|
||||||
"@angular-devkit/build-angular": "^15.0.3",
|
|
||||||
"@angular/cli": "~15.0.2",
|
|
||||||
"@angular/compiler-cli": "^15.0.0",
|
|
||||||
"@types/jasmine": "~4.3.0",
|
|
||||||
"@types/oauth": "^0.9.1",
|
|
||||||
"@types/object-assign-deep": "^0.4.0",
|
|
||||||
"@types/parse-link-header": "^2.0.0",
|
|
||||||
"jasmine-core": "~4.5.0",
|
|
||||||
"karma": "~6.4.0",
|
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
|
||||||
"karma-coverage": "~2.2.0",
|
|
||||||
"karma-jasmine": "~5.1.0",
|
|
||||||
"karma-jasmine-html-reporter": "~2.0.0",
|
|
||||||
"ng-packagr": "^15.0.0",
|
|
||||||
"typescript": "~4.8.2"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@nebular/auth@10.0.0": {
|
|
||||||
"@angular/animations": "15.0.3",
|
|
||||||
"@angular/cdk": "15.0.0",
|
|
||||||
"@angular/common": "15.0.3",
|
|
||||||
"@angular/core": "15.0.3",
|
|
||||||
"@angular/forms": "15.0.3",
|
|
||||||
"@angular/router": "15.0.3"
|
|
||||||
},
|
},
|
||||||
"@nebular/theme@10.0.0": {
|
"private": true,
|
||||||
"@angular/animations": "15.0.3",
|
"dependencies": {
|
||||||
"@angular/cdk": "15.0.0",
|
"@angular/animations": "^15.0.0",
|
||||||
"@angular/common": "15.0.3",
|
"@angular/common": "^15.0.0",
|
||||||
"@angular/core": "15.0.3",
|
"@angular/compiler": "^15.0.0",
|
||||||
"@angular/router": "15.0.3"
|
"@angular/core": "^15.0.0",
|
||||||
|
"@angular/forms": "^15.0.0",
|
||||||
|
"@angular/platform-browser": "^15.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^15.0.0",
|
||||||
|
"@angular/router": "^15.0.0",
|
||||||
|
"@nebular/auth": "10.0.0",
|
||||||
|
"@nebular/eva-icons": "^10.0.0",
|
||||||
|
"@nebular/security": "10.0.0",
|
||||||
|
"@nebular/theme": "10.0.0",
|
||||||
|
"@ngrx/effects": "^15.0.0",
|
||||||
|
"@ngrx/entity": "^15.0.0",
|
||||||
|
"@ngrx/store": "^15.0.0",
|
||||||
|
"@ngrx/store-devtools": "^15.0.0",
|
||||||
|
"axios": "^1.2.1",
|
||||||
|
"eva-icons": "^1.1.3",
|
||||||
|
"ngx-progressbar": "^9.0.0",
|
||||||
|
"oauth": "^0.10.0",
|
||||||
|
"object-assign-deep": "^0.4.0",
|
||||||
|
"parse-link-header": "^2.0.0",
|
||||||
|
"rxjs": "~7.5.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.12.0"
|
||||||
},
|
},
|
||||||
"@nebular/security@10.0.0": {
|
"devDependencies": {
|
||||||
"@angular/common": "15.0.3",
|
"@angular-builders/custom-webpack": "^15.0.0-beta.0",
|
||||||
"@angular/core": "15.0.3",
|
"@angular-devkit/build-angular": "^15.0.3",
|
||||||
"@angular/router": "15.0.3"
|
"@angular/cli": "~15.0.2",
|
||||||
|
"@angular/compiler-cli": "^15.0.0",
|
||||||
|
"@types/jasmine": "~4.3.0",
|
||||||
|
"@types/oauth": "^0.9.1",
|
||||||
|
"@types/object-assign-deep": "^0.4.0",
|
||||||
|
"@types/parse-link-header": "^2.0.0",
|
||||||
|
"jasmine-core": "~4.5.0",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.0.0",
|
||||||
|
"ng-packagr": "^15.0.0",
|
||||||
|
"typescript": "~4.8.2"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@nebular/auth@10.0.0": {
|
||||||
|
"@angular/animations": "15.0.3",
|
||||||
|
"@angular/cdk": "15.0.0",
|
||||||
|
"@angular/common": "15.0.3",
|
||||||
|
"@angular/core": "15.0.3",
|
||||||
|
"@angular/forms": "15.0.3",
|
||||||
|
"@angular/router": "15.0.3"
|
||||||
|
},
|
||||||
|
"@nebular/theme@10.0.0": {
|
||||||
|
"@angular/animations": "15.0.3",
|
||||||
|
"@angular/cdk": "15.0.0",
|
||||||
|
"@angular/common": "15.0.3",
|
||||||
|
"@angular/core": "15.0.3",
|
||||||
|
"@angular/router": "15.0.3"
|
||||||
|
},
|
||||||
|
"@nebular/security@10.0.0": {
|
||||||
|
"@angular/common": "15.0.3",
|
||||||
|
"@angular/core": "15.0.3",
|
||||||
|
"@angular/router": "15.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Injectable} from '@angular/core';
|
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 {RegisterAppResponse} from "../interfaces/responses/register_app_response";
|
||||||
import {GetAccessTokenResponse} from "../interfaces/responses/get_access_token_response";
|
import {GetAccessTokenResponse} from "../interfaces/responses/get_access_token_response";
|
||||||
import {VerifyCredentialsResponse} from "../interfaces/responses/verify_credentials_response";
|
import {VerifyCredentialsResponse} from "../interfaces/responses/verify_credentials_response";
|
||||||
@@ -31,17 +31,20 @@ export class MastodonApiAuthenticationService {
|
|||||||
const url = `https://${instance}/api/v1/apps`;
|
const url = `https://${instance}/api/v1/apps`;
|
||||||
return this.mastodonApiService
|
return this.mastodonApiService
|
||||||
.post<RegisterAppResponse>(url, parameters)
|
.post<RegisterAppResponse>(url, parameters)
|
||||||
.pipe(map((response) => {
|
.pipe(
|
||||||
return <RegisteredApp>{
|
map((response) => {
|
||||||
id: response.id,
|
return <RegisteredApp>{
|
||||||
name: response.name,
|
id: response.id,
|
||||||
website: response.website,
|
name: response.name,
|
||||||
redirectUri: response.redirect_uri,
|
website: response.website,
|
||||||
clientId: response.client_id,
|
redirectUri: response.redirect_uri,
|
||||||
clientSecret: response.client_secret,
|
clientId: response.client_id,
|
||||||
vapidKey: response.vapid_key,
|
clientSecret: response.client_secret,
|
||||||
}
|
vapidKey: response.vapid_key,
|
||||||
}));
|
}
|
||||||
|
}),
|
||||||
|
catchError((error) => throwError(error)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizeUser(instance: string, clientId: string, redirectUrl: string) {
|
authorizeUser(instance: string, clientId: string, redirectUrl: string) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {AuthGuard} from './shared/guards/auth.guard';
|
import {AuthGuard} from './authorization/guards/auth.guard';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ const routes: Routes = [
|
|||||||
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
|
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'auth',
|
path: 'authorize',
|
||||||
loadChildren: () => import('./authorization/authorization.module').then(m => m.AuthorizationModule),
|
loadChildren: () => import('./authorization/authorization.module').then(m => m.AuthorizationModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<a [routerLink]="['/']">
|
<a [routerLink]="['/']">
|
||||||
<img height="40px" src="assets/images/novaloop.png" alt="Novaloop favicon">
|
<img height="40px" src="assets/images/novaloop.png" alt="Novaloop favicon">
|
||||||
</a>
|
</a>
|
||||||
<h2>Mastolists</h2>
|
<h2>{{appName}}</h2>
|
||||||
</div>
|
</div>
|
||||||
<!--add class button-responsive to the button-->
|
<!--add class button-responsive to the button-->
|
||||||
<button nbButton ghost class="button-responsive" [nbContextMenu]="navigationItems">
|
<button nbButton ghost class="button-responsive" [nbContextMenu]="navigationItems">
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
Novaloop AG<br/>
|
Novaloop AG<br/>
|
||||||
Niederdorfstrasse 88<br/>
|
Niederdorfstrasse 88<br/>
|
||||||
8001 Zürich<br/>
|
8001 Zürich<br/>
|
||||||
|
<span class="version-number">v{{version}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-center">
|
<div class="col col-center">
|
||||||
<div class="mastodon-link">
|
<div class="mastodon-link">
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.version-number {
|
||||||
|
color: darkgray;
|
||||||
|
}
|
||||||
|
|
||||||
img.mastodon-logo {
|
img.mastodon-logo {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import {Component, isDevMode} from '@angular/core';
|
import {Component, isDevMode} from '@angular/core';
|
||||||
import {Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {MastodonApiActions} from "./shared/state/store/actions";
|
import {fromApplication, fromAuthorize, selectIsLoggedIn} from "./shared/state/store";
|
||||||
import {PersistentStore} from "./shared/state/persistent/persistent-store.service";
|
import {environment} from "../environments/environment";
|
||||||
|
// @ts-ignore
|
||||||
|
import packageJson from '../../../../package.json';
|
||||||
|
import {tap} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -9,10 +12,11 @@ import {PersistentStore} from "./shared/state/persistent/persistent-store.servic
|
|||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'mastolists';
|
appName = environment.appName;
|
||||||
|
version = packageJson.version;
|
||||||
|
|
||||||
navigationItems = [
|
navigationItems = [
|
||||||
{title: 'Authorize', link: '/auth'},
|
{title: 'Authorize', link: '/authorize'},
|
||||||
{title: 'Stats', link: '/sync'},
|
{title: 'Stats', link: '/sync'},
|
||||||
{title: 'Edit Lists', link: '/lists'},
|
{title: 'Edit Lists', link: '/lists'},
|
||||||
{title: 'List view', link: '/followings/list'},
|
{title: 'List view', link: '/followings/list'},
|
||||||
@@ -20,10 +24,15 @@ export class AppComponent {
|
|||||||
{title: 'Table View', link: '/followings/table'},
|
{title: 'Table View', link: '/followings/table'},
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
constructor(private store: Store) {
|
||||||
if (this.persistentStore.isAuthorized() && !isDevMode()) {
|
this.store.dispatch(fromAuthorize.loadLocalStorage());
|
||||||
this.store.dispatch(MastodonApiActions.loadLists());
|
this.store.pipe(
|
||||||
this.store.dispatch(MastodonApiActions.loadFollowings());
|
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 {MastodonApiModule} from "../../../mastodon-api/src/lib/mastodon-api.module";
|
||||||
import {
|
import {
|
||||||
NbActionsModule,
|
NbActionsModule,
|
||||||
NbContextMenuModule, NbDialogModule,
|
NbContextMenuModule,
|
||||||
|
NbDialogModule,
|
||||||
NbGlobalPhysicalPosition,
|
NbGlobalPhysicalPosition,
|
||||||
NbLayoutModule,
|
NbLayoutModule,
|
||||||
NbMenuModule, NbProgressBarModule,
|
NbMenuModule,
|
||||||
NbThemeModule,
|
NbThemeModule,
|
||||||
NbToastrModule
|
NbToastrModule
|
||||||
} from "@nebular/theme";
|
} from "@nebular/theme";
|
||||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||||
import {SharedModule} from './shared/shared.module';
|
import {SharedModule} from './shared/shared.module';
|
||||||
import {NbEvaIconsModule} from "@nebular/eva-icons";
|
import {NbEvaIconsModule} from "@nebular/eva-icons";
|
||||||
import {StoreModule} from '@ngrx/store';
|
import {ActionReducer, StoreModule} from '@ngrx/store';
|
||||||
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
|
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 {EffectsModule} from "@ngrx/effects";
|
||||||
import {ApplicationEffects} from "./shared/state/store/effects";
|
|
||||||
import {NgProgressModule} from "ngx-progressbar";
|
import {NgProgressModule} from "ngx-progressbar";
|
||||||
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
||||||
|
import {AuthorizationModule} from "./authorization/authorization.module";
|
||||||
|
import {ApplicationStateEffects, AuthorizationStateEffects, fromAuthorize} from "./shared/state/store";
|
||||||
|
|
||||||
const toastrConfig = {
|
const toastrConfig = {
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
@@ -31,6 +33,28 @@ const toastrConfig = {
|
|||||||
destroyByClick: true,
|
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({
|
@NgModule({
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -41,9 +65,13 @@ const toastrConfig = {
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
MastodonApiModule.forRoot(),
|
MastodonApiModule.forRoot(),
|
||||||
|
AuthorizationModule.forRoot(),
|
||||||
StoreModule.forRoot({}),
|
StoreModule.forRoot({}),
|
||||||
StoreModule.forFeature('applicationState', applicationStateReducers),
|
StoreModule.forFeature('authentication', authenticationStateReducer, {metaReducers}),
|
||||||
EffectsModule.forRoot(ApplicationEffects),
|
StoreModule.forFeature('application', applicationStateReducer),
|
||||||
|
EffectsModule.forRoot(),
|
||||||
|
EffectsModule.forFeature(ApplicationStateEffects),
|
||||||
|
EffectsModule.forFeature(AuthorizationStateEffects),
|
||||||
StoreDevtoolsModule.instrument({
|
StoreDevtoolsModule.instrument({
|
||||||
maxAge: 25,
|
maxAge: 25,
|
||||||
logOnly: false,
|
logOnly: false,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import {ModuleWithProviders, NgModule} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {AuthorizationRoutingModule} from "./authorization-routing.module";
|
import {AuthorizationRoutingModule} from "./authorization-routing.module";
|
||||||
import {AuthorizeComponent} from './authorize/authorize.component';
|
import {AuthorizeComponent} from './authorize/authorize.component';
|
||||||
import {ReactiveFormsModule} from '@angular/forms';
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {NbSpinnerModule} from "@nebular/theme";
|
import {NbSpinnerModule} from "@nebular/theme";
|
||||||
|
import {AuthGuard} from "./guards/auth.guard";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -20,4 +21,12 @@ import {NbSpinnerModule} from "@nebular/theme";
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AuthorizationModule {
|
export class AuthorizationModule {
|
||||||
|
static forRoot(): ModuleWithProviders<AuthorizationModule> {
|
||||||
|
return {
|
||||||
|
ngModule: AuthorizationModule,
|
||||||
|
providers: [
|
||||||
|
AuthGuard,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,27 @@
|
|||||||
</nb-card-header>
|
</nb-card-header>
|
||||||
<nb-card-body>
|
<nb-card-body>
|
||||||
<div [formGroup]="serverForm">
|
<div [formGroup]="serverForm">
|
||||||
<input nbInput id="serverUrl" type="text" placeholder="mastodon.social" [formControlName]="'instance'">
|
<input nbInput id="serverUrl" type="text" placeholder="mastodon.social" [formControlName]="'instance'" (keydown.enter)="registerApplication()">
|
||||||
<button class="authorize-button" nbButton type="button"
|
<button class="button" nbButton type="button"
|
||||||
[nbSpinner]="authorizing"
|
[nbSpinner]="(registeringApplication$ | async)!"
|
||||||
(click)="registerApplication()">Authorize
|
[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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isAuthorized.value">
|
<div *ngIf="(loggedIn$ | async) && (instanceName$ | async) as instanceName">
|
||||||
<p>User is authorized with {{currentInstance?.instanceName}}</p>
|
<p>User is authorized with {{instanceName}}</p>
|
||||||
<p>Next up: <a routerLink="/sync">Sync</a></p>
|
<p>Next up: Wait for <a routerLink="/sync">Sync</a> and manage</p>
|
||||||
</div>
|
</div>
|
||||||
</nb-card-body>
|
</nb-card-body>
|
||||||
</nb-card>
|
</nb-card>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
.authorize-button {
|
.button {
|
||||||
margin: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 573px) {
|
@media (max-width: 573px) {
|
||||||
.authorize-button {
|
.button {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {FormControl, FormGroup} from "@angular/forms";
|
import {FormControl, FormGroup, Validators} from "@angular/forms";
|
||||||
import {ActivatedRoute, Params, Router} from "@angular/router";
|
import {ActivatedRoute, Params} from "@angular/router";
|
||||||
import {BehaviorSubject, filter, Subscription, take} from "rxjs";
|
import {concat, filter, map, merge, Observable, take, tap, withLatestFrom, zip, zipWith} from "rxjs";
|
||||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {MastodonApiAccountsService, MastodonApiAuthenticationService, MastodonApiListsService} from "projects/mastodon-api/src/public-api";
|
import {
|
||||||
import {Instance} from "../../shared/state/persistent/state";
|
selectAuthorizingUserFlag,
|
||||||
|
selectCurrentInstance,
|
||||||
|
fromAuthorize,
|
||||||
|
selectIsLoggedIn,
|
||||||
|
selectRegisteringApplicationFlag,
|
||||||
|
selectDataForAuthorizationFlow, selectApplicationRegisteredFlag, selectCurrentInstanceWithApplicationRegisteredState
|
||||||
|
} from "../../shared/state/store";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-authorize',
|
selector: 'app-authorize',
|
||||||
@@ -12,109 +18,86 @@ import {Instance} from "../../shared/state/persistent/state";
|
|||||||
styleUrls: ['./authorize.component.scss']
|
styleUrls: ['./authorize.component.scss']
|
||||||
})
|
})
|
||||||
export class AuthorizeComponent implements OnInit, OnDestroy {
|
export class AuthorizeComponent implements OnInit, OnDestroy {
|
||||||
subscriptions: Subscription[] = [];
|
registeringApplication$: Observable<boolean>;
|
||||||
currentInstance?: Instance;
|
applicationRegistered$: Observable<boolean>;
|
||||||
accessToken?: string;
|
authorizingUser$: Observable<boolean>;
|
||||||
accountId?: string;
|
loggedIn$: Observable<boolean>;
|
||||||
isAuthorized: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
instanceName$: Observable<string | undefined>;
|
||||||
authorizing: boolean = false;
|
|
||||||
|
|
||||||
serverForm = new FormGroup({
|
serverForm = new FormGroup({
|
||||||
instance: new FormControl(''),
|
instance: new FormControl('', [Validators.required]),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private mastodonApiAuthService: MastodonApiAuthenticationService,
|
constructor(private store: Store, private route: ActivatedRoute) {
|
||||||
private store: PersistentStore,
|
this.registeringApplication$ = this.store.pipe(select(selectRegisteringApplicationFlag));
|
||||||
private mastodonApiAccountsService: MastodonApiAccountsService,
|
this.applicationRegistered$ = this.store.pipe(select(selectApplicationRegisteredFlag));
|
||||||
private mastodonApiListsService: MastodonApiListsService,
|
this.authorizingUser$ = this.store.pipe(select(selectAuthorizingUserFlag));
|
||||||
private router: Router,
|
this.loggedIn$ = this.store.pipe(select(selectIsLoggedIn));
|
||||||
private route: ActivatedRoute,) {
|
this.instanceName$ = this.store.pipe(
|
||||||
this.subscriptions.push(this.store
|
select(selectCurrentInstanceWithApplicationRegisteredState),
|
||||||
.select('currentInstance')
|
tap((result) => {
|
||||||
.subscribe((currentInstance) => {
|
if (result.instanceName !== undefined && result.applicationRegistered) {
|
||||||
this.currentInstance = <Instance>currentInstance
|
this.serverForm.get('instance')?.setValue(result.instanceName);
|
||||||
this.accessToken = this.currentInstance?.accessToken;
|
this.serverForm.get('instance')?.disable();
|
||||||
this.accountId = this.currentInstance?.accountId;
|
} else {
|
||||||
this.isAuthorized.next(!!(this.currentInstance && this.accessToken && this.accountId));
|
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 {
|
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() {
|
async registerApplication() {
|
||||||
this.authorizing = true;
|
const instanceName = this.serverForm.get('instance')?.value;
|
||||||
this.subscriptions.push(this.store
|
if (instanceName) {
|
||||||
.select<Instance>('currentInstance')
|
const redirectUrl = window.location.protocol + '//' + window.location.host + window.location.pathname;
|
||||||
.subscribe((currentInstance) => {
|
this.store.dispatch(fromAuthorize.registerApplication({instanceName, redirectUrl}));
|
||||||
this.authorizing = false;
|
}
|
||||||
let instanceName = this.serverForm.value.instance ?? '';
|
|
||||||
if (instanceName === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 {
|
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 {Component} from '@angular/core';
|
||||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||||
import {Account, List, MastodonApiListsService} from 'projects/mastodon-api/src/public-api';
|
|
||||||
import {NbToastrService} from "@nebular/theme";
|
|
||||||
import {ListService} from "../../shared/services/list.service";
|
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {selectFilteredFollowingsWithLists, selectFollowings, selectFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||||
import {FiltersActions, ListActions, MastodonApiActions} from "../../shared/state/store/actions";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
@@ -23,11 +19,11 @@ export class ListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addAccountToSelectedList(accountId: string, select: HTMLSelectElement) {
|
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) {
|
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 {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 {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
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 {NbDialogService} from "@nebular/theme";
|
||||||
|
import {fromMatrixViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||||
|
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-matrix',
|
selector: 'app-matrix',
|
||||||
@@ -36,9 +32,9 @@ export class MatrixComponent {
|
|||||||
|
|
||||||
onCheckedChange($event: boolean, accountId: string, listId: string) {
|
onCheckedChange($event: boolean, accountId: string, listId: string) {
|
||||||
if ($event) {
|
if ($event) {
|
||||||
this.store.dispatch(ListActions.addAccountToList({accountId, listId}));
|
this.store.dispatch(fromMatrixViewPage.addAccountToList({accountId, listId}));
|
||||||
} else {
|
} 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 {List} from "../../../../../mastodon-api/src/lib/interfaces/public/list";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
import {selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store/selectors";
|
||||||
import {ListActions} from "../../shared/state/store/actions";
|
import {fromTableViewPage} from "../../shared/state/store";
|
||||||
|
|
||||||
interface DataGridRow {
|
interface DataGridRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -49,10 +49,10 @@ export class TableComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addAccountToSelectedList(accountId: string, listId: string) {
|
addAccountToSelectedList(accountId: string, listId: string) {
|
||||||
this.store.dispatch(ListActions.addAccountToList({accountId, listId}));
|
this.store.dispatch(fromTableViewPage.addAccountToList({accountId, listId}));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAccountFromList(accountId: string, listId: string) {
|
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>
|
<p>Organize your Followings into lists.</p>
|
||||||
|
|
||||||
<ul>
|
<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><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>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>
|
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nb-card-header>
|
</nb-card-header>
|
||||||
<nb-card-body>
|
<nb-card-body>
|
||||||
<table>
|
<table *ngIf="(instanceName$ | async) as instanceName">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Id</th>
|
<th>Id</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import {Component} from '@angular/core';
|
|||||||
import {List} from 'projects/mastodon-api/src/public-api';
|
import {List} from 'projects/mastodon-api/src/public-api';
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {selectLists} from "../../shared/state/store/selectors";
|
import {selectCurrentInstance, selectLists} from "../../shared/state/store/selectors";
|
||||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
|
||||||
import {ListActions} from "../../shared/state/store/actions";
|
|
||||||
import {FormControl, FormGroup} from "@angular/forms";
|
import {FormControl, FormGroup} from "@angular/forms";
|
||||||
|
import {fromListsPage} from '../../shared/state/store';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lists',
|
selector: 'app-lists',
|
||||||
@@ -15,28 +14,28 @@ import {FormControl, FormGroup} from "@angular/forms";
|
|||||||
export class ListsComponent {
|
export class ListsComponent {
|
||||||
|
|
||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<ReadonlyArray<List>>;
|
||||||
instanceName: string;
|
instanceName$: Observable<string | undefined>;
|
||||||
creatingList: boolean = false;
|
creatingList: boolean = false;
|
||||||
|
|
||||||
newListForm = new FormGroup({
|
newListForm = new FormGroup({
|
||||||
title: new FormControl(''),
|
title: new FormControl(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
constructor(private store: Store) {
|
||||||
this.instanceName = persistentStore.value.currentInstance.instanceName;
|
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
}
|
}
|
||||||
|
|
||||||
listNameChanged(id: string, event: Event) {
|
listNameChanged(id: string, event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const newTitle = target.value;
|
const newTitle = target.value;
|
||||||
this.store.dispatch(ListActions.updateList({listId: id, newTitle}));
|
this.store.dispatch(fromListsPage.updateList({listId: id, newTitle}));
|
||||||
}
|
}
|
||||||
|
|
||||||
createList() {
|
createList() {
|
||||||
let title = this.newListForm.value.title ?? '';
|
let title = this.newListForm.value.title ?? '';
|
||||||
if (title.length > 0) {
|
if (title.length > 0) {
|
||||||
this.store.dispatch(ListActions.createList({title}));
|
this.store.dispatch(fromListsPage.createList({title}));
|
||||||
this.newListForm.reset();
|
this.newListForm.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import {Component, OnDestroy} from '@angular/core';
|
import {Component, OnDestroy} from '@angular/core';
|
||||||
import {FormControl, FormGroup} from "@angular/forms";
|
import {FormControl, FormGroup} from "@angular/forms";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {FiltersActions, MastodonApiActions} from "../../state/store/actions";
|
|
||||||
import {debounceTime, Observable, Subscription, tap} from "rxjs";
|
import {debounceTime, Observable, Subscription, tap} from "rxjs";
|
||||||
import {List} from 'projects/mastodon-api/src/public-api';
|
import {List} from 'projects/mastodon-api/src/public-api';
|
||||||
import {selectFilters, selectFiltersForForm, selectLists} from "../../state/store/selectors";
|
import {fromFilters, selectFiltersForForm, selectLists} from "../../state/store";
|
||||||
import {Filters} from "../../state/store/reducers";
|
|
||||||
|
|
||||||
interface FiltersForForm {
|
interface FiltersForForm {
|
||||||
fullText: string | undefined;
|
fullText: string | undefined;
|
||||||
@@ -43,11 +41,11 @@ export class FiltersComponent implements OnDestroy {
|
|||||||
.pipe(debounceTime(500))
|
.pipe(debounceTime(500))
|
||||||
.subscribe(value => {
|
.subscribe(value => {
|
||||||
if (value['lists'] && value['lists'].length > 0) {
|
if (value['lists'] && value['lists'].length > 0) {
|
||||||
this.store.dispatch(FiltersActions.setLists({lists: value.lists}));
|
this.store.dispatch(fromFilters.setLists({lists: value.lists}));
|
||||||
this.store.dispatch(FiltersActions.setUnlisted({unlisted: false}));
|
this.store.dispatch(fromFilters.setUnlisted({unlisted: false}));
|
||||||
}
|
}
|
||||||
if (!value['lists'] || value['lists'].length === 0) {
|
if (!value['lists'] || value['lists'].length === 0) {
|
||||||
this.store.dispatch(FiltersActions.setLists({lists: []}));
|
this.store.dispatch(fromFilters.setLists({lists: []}));
|
||||||
}
|
}
|
||||||
if (value['text']) {
|
if (value['text']) {
|
||||||
const text = value['text'];
|
const text = value['text'];
|
||||||
@@ -55,18 +53,18 @@ export class FiltersComponent implements OnDestroy {
|
|||||||
const parts = text.split(/[\s,]+/);
|
const parts = text.split(/[\s,]+/);
|
||||||
if (parts.some((part: string) => part.startsWith('@'))) {
|
if (parts.some((part: string) => part.startsWith('@'))) {
|
||||||
const username = parts.find((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 {
|
} 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 {
|
} else {
|
||||||
this.store.dispatch(FiltersActions.setUsername({username: ''}));
|
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||||
this.store.dispatch(FiltersActions.setFreeText({freeText: []}));
|
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.store.dispatch(FiltersActions.setUsername({username: ''}));
|
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||||
this.store.dispatch(FiltersActions.setFreeText({freeText: []}));
|
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -76,10 +74,11 @@ export class FiltersComponent implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
this.store.dispatch(FiltersActions.clearFilters());
|
this.store.dispatch(fromFilters.clearFilters());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleUnlisted($event: boolean) {
|
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 {Injectable, OnDestroy} from '@angular/core';
|
||||||
import {MastodonApiAccountsService} from "../../../../../mastodon-api/src/lib/services/mastodon-api-accounts.service";
|
import {EMPTY, expand, map, Observable, reduce, Subscription, switchMap, tap} from "rxjs";
|
||||||
import {PersistentStore} from "../state/persistent/persistent-store.service";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {EMPTY, expand, map, Observable, reduce} from "rxjs";
|
import {Account, MastodonApiAccountsService} from 'projects/mastodon-api/src/public-api';
|
||||||
import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/account";
|
import {selectDataForAuthorizedRequest} from "../state/store";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
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,
|
constructor(private mastodonApiAccountsService: MastodonApiAccountsService, private store: Store) {
|
||||||
private store: PersistentStore) {
|
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>> {
|
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
|
return this.mastodonApiAccountsService
|
||||||
.getFollowingsForAccount(instanceName, accessToken!, accountId)
|
.getFollowingsForAccount(this.instanceName!, this.accessToken!, this.accountId!)
|
||||||
.pipe(
|
.pipe(
|
||||||
expand(result => {
|
expand(result => {
|
||||||
const nextLink = result[0];
|
const nextLink = result[0];
|
||||||
if (nextLink && nextLink.length > 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;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,63 +1,47 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable, OnDestroy} from '@angular/core';
|
||||||
import {EMPTY, expand, map, Observable, reduce, take} from "rxjs";
|
import {EMPTY, expand, filter, map, Observable, of, reduce, Subscription, switchMap, tap} from "rxjs";
|
||||||
import {MastodonApiListsService} from "../../../../../mastodon-api/src/lib/services/mastodon-api-lists.service";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {PersistentStore} from "../state/persistent/persistent-store.service";
|
import {List, MastodonApiListsService} from 'projects/mastodon-api/src/public-api';
|
||||||
import {Account} from "../../../../../mastodon-api/src/lib/interfaces/public/account";
|
import {selectDataForAuthorizedRequest} from "../state/store";
|
||||||
import {List} from "../../../../../mastodon-api/src/lib/interfaces/public/list";
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ListService {
|
export class ListService implements OnDestroy {
|
||||||
|
|
||||||
constructor(private mastodonApiListsService: MastodonApiListsService,
|
private instanceName: string | undefined;
|
||||||
private store: PersistentStore) {
|
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[]> {
|
loadLists(): Observable<List[]> {
|
||||||
const applicationState = this.store.value;
|
return this.mastodonApiListsService.getLists(this.instanceName!, this.accessToken!)
|
||||||
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))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createList(title: string) {
|
createList(title: string) {
|
||||||
const applicationState = this.store.value;
|
return this.mastodonApiListsService.createList(this.instanceName!, this.accessToken!, title);
|
||||||
const instanceName = applicationState.currentInstance?.instanceName;
|
|
||||||
const accessToken = applicationState.currentInstance?.accessToken;
|
|
||||||
return this.mastodonApiListsService
|
|
||||||
.createList(instanceName, accessToken!, title)
|
|
||||||
.pipe(
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateList(listId: string, newName: string) {
|
updateList(listId: string, newName: string) {
|
||||||
const applicationState = this.store.value;
|
return this.mastodonApiListsService.updateList(this.instanceName!, this.accessToken!, listId, newName);
|
||||||
const instanceName = applicationState.currentInstance?.instanceName;
|
|
||||||
const accessToken = applicationState.currentInstance?.accessToken;
|
|
||||||
return this.mastodonApiListsService
|
|
||||||
.updateList(instanceName, accessToken!, listId, newName)
|
|
||||||
.pipe(
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
loadAccountsIdsForList(listId: string): Observable<{ [id: string]: string[] }> {
|
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
|
return this.mastodonApiListsService
|
||||||
.getAccountsForList(instanceName, accessToken!, listId)
|
.getAccountsForList(this.instanceName!, this.accessToken!, listId)
|
||||||
.pipe(
|
.pipe(
|
||||||
expand(result => {
|
expand(result => {
|
||||||
const nextLink = result[0];
|
const nextLink = result[0];
|
||||||
if (nextLink && nextLink.length > 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;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
@@ -78,25 +62,15 @@ export class ListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addAccountToSelectedList(accountId: string, listId: string) {
|
addAccountToSelectedList(accountId: string, listId: string) {
|
||||||
const applicationState = this.store.value;
|
return this.mastodonApiListsService.addAccountToList(this.instanceName!, this.accessToken!, listId, accountId)
|
||||||
const instanceName = applicationState.currentInstance?.instanceName;
|
|
||||||
const accessToken = applicationState.currentInstance?.accessToken;
|
|
||||||
return this.mastodonApiListsService
|
|
||||||
.addAccountToList(instanceName, accessToken!, listId, accountId)
|
|
||||||
.pipe(
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAccountFromList(accountId: string, listId: string) {
|
removeAccountFromList(accountId: string, listId: string) {
|
||||||
const applicationState = this.store.value;
|
return this.mastodonApiListsService.removeAccountFromList(this.instanceName!, this.accessToken!, listId, accountId)
|
||||||
const instanceName = applicationState.currentInstance?.instanceName;
|
}
|
||||||
const accessToken = applicationState.currentInstance?.accessToken;
|
|
||||||
return this.mastodonApiListsService
|
ngOnDestroy(): void {
|
||||||
.removeAccountFromList(instanceName, accessToken!, listId, accountId)
|
this.storeSubscription.unsubscribe();
|
||||||
.pipe(
|
|
||||||
take(1),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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} from "@ngrx/store";
|
||||||
import {createFeatureSelector, createSelector, State} from "@ngrx/store";
|
import {ApplicationState} from "../application-state";
|
||||||
|
|
||||||
|
export const applicationStateFeature = createFeatureSelector<ApplicationState>('application');
|
||||||
export const applicationStateFeature = createFeatureSelector<ApplicationState>('applicationState');
|
|
||||||
|
|
||||||
export const selectLists = createSelector(
|
export const selectLists = createSelector(
|
||||||
applicationStateFeature,
|
applicationStateFeature,
|
||||||
(state: ApplicationState) => state.lists
|
(state: ApplicationState) => [...state.lists].sort((a, b) => a.title.localeCompare(b.title))
|
||||||
)
|
)
|
||||||
export const selectFollowings = createSelector(
|
export const selectFollowings = createSelector(
|
||||||
applicationStateFeature,
|
applicationStateFeature,
|
||||||
@@ -24,7 +23,7 @@ export const selectListsWithAccounts = createSelector(
|
|||||||
selectMappings,
|
selectMappings,
|
||||||
(lists, followings, mappings) => {
|
(lists, followings, mappings) => {
|
||||||
return lists.map(list => {
|
return lists.map(list => {
|
||||||
const accountIds = mappings[list.id];
|
const accountIds = mappings[list.id] || [];
|
||||||
const accounts = followings.filter(account => accountIds.includes(account.id));
|
const accounts = followings.filter(account => accountIds.includes(account.id));
|
||||||
return {...list, accounts};
|
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 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">
|
<a [href]="'https://' + instanceName + '/settings/export'" rel="noreferrer" target="_blank">
|
||||||
Export lists using your account settings page
|
Export lists using your account settings page
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {MastodonApiActions} from "../../shared/state/store/actions";
|
import {selectCurrentInstance, fromSyncPage, selectFollowings, selectListsWithAccounts, selectLoading} from "../../shared/state/store";
|
||||||
import {selectFollowings, selectLists, selectListsWithAccounts, selectLoading} from "../../shared/state/store/selectors";
|
import {Observable} from "rxjs";
|
||||||
import {Observable, tap} from "rxjs";
|
|
||||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||||
import {PersistentStore} from "../../shared/state/persistent/persistent-store.service";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sync',
|
selector: 'app-sync',
|
||||||
@@ -15,18 +13,18 @@ export class SyncComponent {
|
|||||||
loading$: Observable<boolean>;
|
loading$: Observable<boolean>;
|
||||||
followings$: Observable<ReadonlyArray<Account>>;
|
followings$: Observable<ReadonlyArray<Account>>;
|
||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<ReadonlyArray<List>>;
|
||||||
instanceName: string;
|
instanceName$: Observable<string | undefined>;
|
||||||
|
|
||||||
constructor(private store: Store, private persistentStore: PersistentStore) {
|
constructor(private store: Store) {
|
||||||
this.instanceName = persistentStore.value.currentInstance.instanceName;
|
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||||
this.followings$ = this.store.pipe(select(selectFollowings))
|
this.followings$ = this.store.pipe(select(selectFollowings))
|
||||||
this.lists$ = this.store.pipe(select(selectListsWithAccounts));
|
this.lists$ = this.store.pipe(select(selectListsWithAccounts));
|
||||||
this.loading$ = this.store.pipe(select(selectLoading));
|
this.loading$ = this.store.pipe(select(selectLoading));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadListsAndAccounts() {
|
loadListsAndAccounts() {
|
||||||
this.store.dispatch(MastodonApiActions.loadLists());
|
this.store.dispatch(fromSyncPage.loadLists());
|
||||||
this.store.dispatch(MastodonApiActions.loadFollowings());
|
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',
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../../out-tsc/app",
|
"outDir": "../../out-tsc/app",
|
||||||
"types": []
|
"types": [],
|
||||||
},
|
"resolveJsonModule": true,
|
||||||
"files": [
|
"allowSyntheticDefaultImports": true
|
||||||
"src/main.ts"
|
},
|
||||||
],
|
"files": [
|
||||||
"include": [
|
"src/main.ts"
|
||||||
"src/**/*.d.ts"
|
],
|
||||||
]
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,46 @@
|
|||||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"lib-mastodon": [
|
"lib-mastodon": [
|
||||||
"dist/lib-mastodon"
|
"dist/lib-mastodon"
|
||||||
],
|
],
|
||||||
"mastodon": [
|
"mastodon": [
|
||||||
"dist/mastodon"
|
"dist/mastodon"
|
||||||
],
|
],
|
||||||
"mastodon-api": [
|
"mastodon-api": [
|
||||||
"dist/mastodon-api"
|
"dist/mastodon-api"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"noFallthroughCasesInSwitch": true,
|
"angularCompilerOptions": {
|
||||||
"sourceMap": true,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
"declaration": false,
|
"strictInjectionParameters": true,
|
||||||
"downlevelIteration": true,
|
"strictInputAccessModifiers": true,
|
||||||
"experimentalDecorators": true,
|
"strictTemplates": true
|
||||||
"moduleResolution": "node",
|
}
|
||||||
"importHelpers": true,
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "ES2022",
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"lib": [
|
|
||||||
"ES2022",
|
|
||||||
"dom"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"angularCompilerOptions": {
|
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
|
||||||
"strictInjectionParameters": true,
|
|
||||||
"strictInputAccessModifiers": true,
|
|
||||||
"strictTemplates": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user