Merge pull request #237 from NicolasConstant/develop

0.21.0 PR
This commit is contained in:
Nicolas Constant 2020-03-07 20:12:18 -05:00 committed by GitHub
commit 1cd2833ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1360 additions and 766 deletions

View File

@ -44,4 +44,4 @@ before_script:
- sleep 3
script:
- npm run dist
- npm run travis

View File

@ -1,145 +1,149 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"sengi": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/sass/styles.scss",
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/sass",
"./node_modules/bootstrap/scss"
]
},
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "sengi:build"
},
"configurations": {
"production": {
"browserTarget": "sengi:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "sengi:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/sass/styles.scss"
],
"assets": [
"src/assets",
"src/favicon.ico"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/sass",
"./node_modules/bootstrap/scss"
]
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"sengi": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico",
"src/manifest.json"
],
"styles": [
"src/sass/styles.scss",
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/sass",
"./node_modules/bootstrap/scss"
]
},
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"serviceWorker": true,
"ngswConfigPath": "src/ngsw-config.json"
}
}
},
"sengi-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "sengi:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "sengi:build"
},
"configurations": {
"production": {
"browserTarget": "sengi:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "sengi:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/sass/styles.scss"
],
"assets": [
"src/assets",
"src/favicon.ico",
"src/manifest.json"
],
"stylePreprocessorOptions": {
"includePaths": [
"./src/sass",
"./node_modules/bootstrap/scss"
]
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"defaultProject": "sengi",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "scss"
"sengi-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "sengi:serve"
}
},
"@schematics/angular:directive": {
"prefix": "app"
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "sengi",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
}
}
}

View File

@ -1,8 +1,4 @@
const { app, Menu, server, BrowserWindow, shell } = require("electron");
const path = require("path");
const url = require("url");
const http = require("http");
const fs = require("fs");
const { app, Menu, BrowserWindow, shell } = require("electron");
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
@ -14,15 +10,19 @@ function createWindow() {
width: 377,
height: 800,
title: "Sengi",
backgroundColor: "#FFF",
useContentSize: true
backgroundColor: "#131925",
useContentSize: true,
// webPreferences: {
// contextIsolation: true,
// nodeIntegration: false,
// nodeIntegrationInWorker: false
// }
});
win.setAutoHideMenuBar(true);
win.setMenuBarVisibility(false);
var server = http.createServer(requestHandler).listen(9527);
const sengiUrl = "http://localhost:9527";
const sengiUrl = "https://sengi.nicolas-constant.com";
win.loadURL(sengiUrl);
const template = [
@ -139,9 +139,6 @@ function createWindow() {
);
}
// Open the DevTools.
// win.webContents.openDevTools()
//open external links to browser
win.webContents.on("new-window", function (event, url) {
event.preventDefault();
@ -157,40 +154,6 @@ function createWindow() {
});
}
function requestHandler(req, res) {
var file = req.url == "/" ? "/index.html" : req.url,
root = __dirname + "/dist",
page404 = root + "/404.html";
if (file.includes("register") || file.includes("home")) file = "/index.html";
getFile(root + file, res, page404);
}
function getFile(filePath, res, page404) {
console.warn(`filePath: ${filePath}`);
fs.exists(filePath, function (exists) {
if (exists) {
fs.readFile(filePath, function (err, contents) {
if (!err) {
res.end(contents);
} else {
console.dir(err);
}
});
} else {
fs.readFile(page404, function (err, contents) {
if (!err) {
res.writeHead(404, { "Content-Type": "text/html" });
res.end(contents);
} else {
console.dir(err);
}
});
}
});
}
app.commandLine.appendSwitch("force-color-profile", "srgb");
@ -228,7 +191,4 @@ app.on("activate", () => {
if (win === null) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
});

1050
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.20.1",
"version": "0.21.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",
@ -12,7 +12,7 @@
"type": "git",
"url": "https://github.com/NicolasConstant/sengi.git"
},
"scripts": {
"scripts": {
"ng": "ng",
"start": "ng serve",
"start-mem": "node --max_old_space_size=5048 ./node_modules/@angular/cli/bin/ng serve",
@ -21,9 +21,11 @@
"test-nowatch": "ng test --watch=false",
"lint": "ng lint",
"e2e": "ng e2e",
"electron": "ng build --prod && electron .",
"electron": "electron .",
"electron-prod": "ng build --prod && electron .",
"electron-debug": "ng build && electron .",
"dist": "npm run build && electron-builder --publish onTagOrDraft"
"dist": "npm run build && electron-builder --publish onTagOrDraft",
"travis": "electron-builder --publish onTagOrDraft"
},
"private": true,
"dependencies": {
@ -36,7 +38,9 @@
"@angular/http": "^7.2.7",
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/pwa": "^0.12.4",
"@angular/router": "^7.2.7",
"@angular/service-worker": "^7.2.7",
"@ctrl/ngx-emoji-mart": "^0.17.0",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.13",
@ -66,7 +70,7 @@
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.2.1",
"electron": "^4.0.6",
"electron": "^8.0.2",
"electron-builder": "^20.39.0",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
@ -89,6 +93,7 @@
"productName": "Sengi",
"appId": "org.sengi.desktop",
"artifactName": "${productName}-${version}-${os}.${ext}",
"npmRebuild": false,
"directories": {
"output": "release"
},

View File

@ -1,4 +1,4 @@
<div class="drag-and-drop" *ngIf="drag" (dragover)="dragover($event)" (drop)="drop($event)"
<div class="drag-and-drop" *ngIf="drag" (dragover)="dragover($event)" (drop)="drop($event)"
[ngClass]="{'drag-and-drop__on-drag': drag2 === true }">
<!-- (dragleave)="dragleave($event)" -->
<div class="drag-and-drop__card">
@ -10,6 +10,12 @@
</div>
</div>
<div class="auto-update" [class.auto-update__activated]="updateAvailable">
<div class="auto-update__display">
<div class="auto-update__display--text">A new version is available!</div> <a href class="auto-update__display--reload" (click)="loadNewVersion()">reload</a> <a href class="auto-update__display--close" (click)="closeAutoUpdate()"><fa-icon [icon]="faTimes"></fa-icon></a>
</div>
</div>
<app-media-viewer id="media-viewer" *ngIf="openedMediaEvent" [openedMediaEvent]="openedMediaEvent"
(closeSubject)="closeMedia()" (dragenter)="dragenter($event)"></app-media-viewer>

View File

@ -97,4 +97,75 @@ app-streams-selection-footer {
right: 0;
bottom: 0;
left: 50px;
}
.auto-update {
transition: all .2s;
transition-timing-function: ease-in;
position: absolute;
height: 70px;
left: 0;
right: 0;
bottom: -80px;
z-index: 999999999;
&__activated {
// opacity: 1;
transition: all .25s;
transition-timing-function: ease-out;
bottom: 0px;
}
&__display {
position: relative;
// height: 30px;
width: 300px;
// margin: 0 auto 30px auto;
margin: auto;
border-radius: 2px;
color: rgba(rgb(0, 4, 24), 1);
background: #ffffff;
box-shadow: 0px 0px 10px rgb(0, 0, 0);
&--text {
display: inline-block;
padding: 5px 10px;
}
&--reload {
transition: all .2s;
position: absolute;
right: 30px;
padding: 5px 10px;
text-decoration: none;
color: #ffffff;
background-color: #3e455f;
&:hover {
background-color: #1d202c;
}
}
&--close {
transition: all .2s;
position: absolute;
right: 0;
display: inline-block;
padding: 5px 10px;
text-decoration: none;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
color: #ffffff;
background-color: #3e455f;
&:hover {
background-color: #1d202c;
}
}
}
}

View File

@ -1,14 +1,25 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription, Observable, Subject } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
import { Select } from '@ngxs/store';
// import { ElectronService } from 'ngx-electron';
import { Select, Store } from '@ngxs/store';
import { faTimes } from "@fortawesome/free-solid-svg-icons";
import { NavigationService, LeftPanelType, OpenLeftPanelEvent } from './services/navigation.service';
import { StreamElement } from './states/streams.state';
import { AccountInfo, AddAccount } from "./states/accounts.state";
import { OpenMediaEvent } from './models/common.model';
import { ToolsService } from './services/tools.service';
import { MediaService } from './services/media.service';
import { ServiceWorkerService } from './services/service-worker.service';
import { AuthService, CurrentAuthProcess } from './services/auth.service';
import { MastodonWrapperService } from './services/mastodon-wrapper.service';
import { TokenData, Account } from './services/models/mastodon.interfaces';
import { NotificationService } from './services/notification.service';
import { AppInfo, RegisteredAppsStateModel } from './states/registered-apps.state';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-root',
@ -16,26 +27,96 @@ import { MediaService } from './services/media.service';
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
faTimes = faTimes;
title = 'Sengi';
floatingColumnActive: boolean;
tutorialActive: boolean;
// mediaViewerActive: boolean = false;
openedMediaEvent: OpenMediaEvent
updateAvailable: boolean;
private authStorageKey: string = 'tempAuth';
private columnEditorSub: Subscription;
private openMediaSub: Subscription;
private streamSub: Subscription;
private dragoverSub: Subscription;
private updateAvailableSub: Subscription;
private paramsSub: Subscription;
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
constructor(
private readonly router: Router,
private readonly notificationService: NotificationService,
private readonly store: Store,
private readonly mastodonService: MastodonWrapperService,
private readonly authService: AuthService,
private readonly activatedRoute: ActivatedRoute,
private readonly serviceWorkerService: ServiceWorkerService,
private readonly toolsService: ToolsService,
private readonly mediaService: MediaService,
private readonly navigationService: NavigationService) {
}
ngOnInit(): void {
this.paramsSub = this.activatedRoute.queryParams.subscribe(params => {
const code = params['code'];
if (!code) {
return;
}
const appDataWrapper = <CurrentAuthProcess>JSON.parse(localStorage.getItem(this.authStorageKey));
if (!appDataWrapper) {
this.notificationService.notify('', 400, 'Something when wrong in the authentication process. Please retry.', true);
this.router.navigate(['/']);
return;
}
const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0];
let usedTokenData: TokenData;
this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
.then((tokenData: TokenData) => {
if(tokenData.refresh_token && !tokenData.created_at){
const nowEpoch = Date.now() / 1000 | 0;
tokenData.created_at = nowEpoch;
}
usedTokenData = tokenData;
return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
})
.then((account: Account) => {
var username = account.username.toLowerCase();
var instance = appDataWrapper.instance.toLowerCase();
if(this.isAccountAlreadyPresent(username, instance)){
this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
this.router.navigate(['/']);
return;
}
const accountInfo = new AccountInfo();
accountInfo.username = username;
accountInfo.instance = instance;
accountInfo.token = usedTokenData;
this.store.dispatch([new AddAccount(accountInfo)])
.subscribe(() => {
localStorage.removeItem(this.authStorageKey);
this.router.navigate(['/']);
});
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, null);
this.router.navigate(['/']);
});
});
this.updateAvailableSub = this.serviceWorkerService.newAppVersionIsAvailable.subscribe((updateAvailable) => {
this.updateAvailable = updateAvailable;
});
this.streamSub = this.streamElements$.subscribe((streams: StreamElement[]) => {
if (streams && streams.length === 0) {
this.tutorialActive = true;
@ -60,12 +141,11 @@ export class AppComponent implements OnInit, OnDestroy {
}
});
this.dragoverSub = this.dragoverSubject
.pipe(
debounceTime(1500)
)
.subscribe(() => {
.subscribe(() => {
this.drag = false;
})
}
@ -75,6 +155,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.columnEditorSub.unsubscribe();
this.openMediaSub.unsubscribe();
this.dragoverSub.unsubscribe();
this.updateAvailableSub.unsubscribe();
this.paramsSub.unsubscribe();
}
closeMedia() {
@ -96,7 +178,6 @@ export class AppComponent implements OnInit, OnDestroy {
return false;
}
dragover(event): boolean {
// console.warn('dragover');
event.stopPropagation();
event.preventDefault();
this.dragoverSubject.next(true);
@ -112,4 +193,29 @@ export class AppComponent implements OnInit, OnDestroy {
this.mediaService.uploadMedia(selectedAccount, files);
return false;
}
loadNewVersion(): boolean {
this.serviceWorkerService.loadNewAppVersion();
return false;
}
closeAutoUpdate(): boolean {
this.updateAvailable = false;
return false;
}
private isAccountAlreadyPresent(username: string, instance: string): boolean{
const accounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
for (let acc of accounts) {
if(acc.instance === instance && acc.username == username){
return true;
}
}
return false;
}
private getAllSavedApps(): AppInfo[] {
const snapshot = <RegisteredAppsStateModel>this.store.snapshot().registeredapps;
return snapshot.apps;
}
}

View File

@ -23,7 +23,7 @@ import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.c
import { StreamsMainDisplayComponent } from "./pages/streams-main-display/streams-main-display.component";
import { StreamComponent } from "./components/stream/stream.component";
import { StreamsSelectionFooterComponent } from "./components/streams-selection-footer/streams-selection-footer.component";
import { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
// import { RegisterNewAccountComponent } from "./pages/register-new-account/register-new-account.component";
import { AuthService } from "./services/auth.service";
import { StreamingService } from "./services/streaming.service";
import { RegisteredAppsState } from "./states/registered-apps.state";
@ -79,13 +79,15 @@ import { ScheduledStatusesComponent } from './components/floating-column/schedul
import { ScheduledStatusComponent } from './components/floating-column/scheduled-statuses/scheduled-status/scheduled-status.component';
import { StreamNotificationsComponent } from './components/stream/stream-notifications/stream-notifications.component';
import { NotificationComponent } from './components/floating-column/manage-account/notifications/notification/notification.component';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: StreamsMainDisplayComponent },
{ path: "register", component: RegisterNewAccountComponent },
{ path: "**", redirectTo: "home" }
{ path: "", component: StreamsMainDisplayComponent },
// { path: "home", component: StreamsMainDisplayComponent },
// { path: "register", component: RegisterNewAccountComponent },
{ path: "**", redirectTo: "" }
];
@NgModule({
@ -96,7 +98,7 @@ const routes: Routes = [
StreamComponent,
StreamsSelectionFooterComponent,
StatusComponent,
RegisterNewAccountComponent,
// RegisterNewAccountComponent,
AccountIconComponent,
FloatingColumnComponent,
ManageAccountComponent,
@ -167,7 +169,8 @@ const routes: Routes = [
]),
NgxsStoragePluginModule.forRoot(),
ContextMenuModule.forRoot(),
HotkeyModule.forRoot()
HotkeyModule.forRoot(),
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
],
providers: [AuthService, NavigationService, NotificationService, MastodonWrapperService, MastodonService, StreamingService],
bootstrap: [AppComponent],

View File

@ -104,12 +104,12 @@ export class AddNewAccountComponent implements OnInit {
} else {
let redirect_uri = this.getLocalHostname();
let userAgent = navigator.userAgent.toLowerCase();
console.log(`userAgent ${userAgent}`);
// let userAgent = navigator.userAgent.toLowerCase();
// console.log(`userAgent ${userAgent}`);
if (userAgent.includes(' electron/')) {
redirect_uri += '/register';
}
// if (userAgent.includes(' electron/')) {
// redirect_uri += '/register';
// }
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://nicolasconstant.github.io/sengi/')
.then((appData: AppData) => {

View File

@ -55,7 +55,12 @@
</div>
<h4 class="panel__subtitle">About</h4>
<p class="version">Sengi version: {{version}}</p>
<p class="version">
Sengi version: {{version}}<br/>
<a href class="version__link" (click)="checkForUpdates()">check for updates</a>
<app-waiting-animation *ngIf="isCheckingUpdates" class="waiting-icon"></app-waiting-animation>
</p>
<h4 class="panel__subtitle">RESET</h4>
<div class="sub-section">

View File

@ -17,6 +17,14 @@
.version {
display: block;
padding: 0 5px;
&__link {
color: rgb(161, 161, 161);
&:hover {
color: #fff;
}
}
}
.sub-section {

View File

@ -5,6 +5,7 @@ import { Howl } from 'howler';
import { environment } from '../../../../environments/environment';
import { ToolsService } from '../../../services/tools.service';
import { UserNotificationService, NotificationSoundDefinition } from '../../../services/user-notification.service';
import { ServiceWorkerService } from '../../../services/service-worker.service';
@Component({
selector: 'app-settings',
@ -13,7 +14,7 @@ import { UserNotificationService, NotificationSoundDefinition } from '../../../s
})
export class SettingsComponent implements OnInit {
notificationSounds: NotificationSoundDefinition[];
notificationSoundId: string;
notificationForm: FormGroup;
@ -28,13 +29,14 @@ export class SettingsComponent implements OnInit {
constructor(
private formBuilder: FormBuilder,
private serviceWorkersService: ServiceWorkerService,
private readonly toolsService: ToolsService,
private readonly userNotificationsService: UserNotificationService) { }
ngOnInit() {
this.version = environment.VERSION;
const settings = this.toolsService.getSettings();
const settings = this.toolsService.getSettings();
this.notificationSounds = this.userNotificationsService.getAllNotificationSounds();
this.notificationSoundId = settings.notificationSoundFileId;
@ -46,18 +48,18 @@ export class SettingsComponent implements OnInit {
this.disableAvatarNotificationsEnabled = settings.disableAvatarNotifications;
this.disableSoundsEnabled = settings.disableSounds;
if(!settings.columnSwitchingWinAlt){
if (!settings.columnSwitchingWinAlt) {
this.columnShortcutEnabled = ColumnShortcut.Ctrl;
} else {
this.columnShortcutEnabled = ColumnShortcut.Win;
}
}
onShortcutChange(id: ColumnShortcut){
onShortcutChange(id: ColumnShortcut) {
this.columnShortcutEnabled = id;
this.columnShortcutChanged = true;
let settings = this.toolsService.getSettings()
let settings = this.toolsService.getSettings()
settings.columnSwitchingWinAlt = id === ColumnShortcut.Win;
this.toolsService.saveSettings(settings);
}
@ -85,19 +87,19 @@ export class SettingsComponent implements OnInit {
return false;
}
onDisableAutofocusChanged(){
onDisableAutofocusChanged() {
let settings = this.toolsService.getSettings();
settings.disableAutofocus = this.disableAutofocusEnabled;
this.toolsService.saveSettings(settings);
this.toolsService.saveSettings(settings);
}
onDisableAvatarNotificationsChanged(){
onDisableAvatarNotificationsChanged() {
let settings = this.toolsService.getSettings();
settings.disableAvatarNotifications = this.disableAvatarNotificationsEnabled;
this.toolsService.saveSettings(settings);
}
onDisableSoundsEnabledChanged(){
onDisableSoundsEnabledChanged() {
let settings = this.toolsService.getSettings();
settings.disableSounds = this.disableSoundsEnabled;
this.toolsService.saveSettings(settings);
@ -109,7 +111,7 @@ export class SettingsComponent implements OnInit {
return false;
}
confirmClearAll(): boolean{
confirmClearAll(): boolean {
localStorage.clear();
location.reload();
return false;
@ -119,10 +121,23 @@ export class SettingsComponent implements OnInit {
this.isCleanningAll = false;
return false;
}
isCheckingUpdates = false;
checkForUpdates(): boolean {
this.isCheckingUpdates = true;
this.serviceWorkersService.checkForUpdates()
.catch(err => {
console.error(err);
})
.then(() => {
this.isCheckingUpdates = false;
});
return false;
}
}
enum ColumnShortcut {
Ctrl = 1,
Ctrl = 1,
Win = 2
}

View File

@ -1,124 +1,124 @@
import { Component, OnInit, Input } from "@angular/core";
import { Store, Select } from '@ngxs/store';
import { ActivatedRoute, Router } from "@angular/router";
import { HttpErrorResponse } from "@angular/common/http";
// import { Component, OnInit, Input } from "@angular/core";
// import { Store, Select } from '@ngxs/store';
// import { ActivatedRoute, Router } from "@angular/router";
// import { HttpErrorResponse } from "@angular/common/http";
import { AuthService, CurrentAuthProcess } from "../../services/auth.service";
import { TokenData, Account } from "../../services/models/mastodon.interfaces";
import { RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
import { AccountInfo, AddAccount, AccountsStateModel } from "../../states/accounts.state";
import { NotificationService } from "../../services/notification.service";
import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
// import { AuthService, CurrentAuthProcess } from "../../services/auth.service";
// import { TokenData, Account } from "../../services/models/mastodon.interfaces";
// import { RegisteredAppsStateModel, AppInfo } from "../../states/registered-apps.state";
// import { AccountInfo, AddAccount, AccountsStateModel } from "../../states/accounts.state";
// import { NotificationService } from "../../services/notification.service";
// import { MastodonWrapperService } from '../../services/mastodon-wrapper.service';
@Component({
selector: "app-register-new-account",
templateUrl: "./register-new-account.component.html",
styleUrls: ["./register-new-account.component.scss"]
})
export class RegisterNewAccountComponent implements OnInit {
@Input() mastodonFullHandle: string;
// @Component({
// selector: "app-register-new-account",
// templateUrl: "./register-new-account.component.html",
// styleUrls: ["./register-new-account.component.scss"]
// })
// export class RegisterNewAccountComponent implements OnInit {
// // @Input() mastodonFullHandle: string;
hasError: boolean;
errorMessage: string;
// hasError: boolean;
// errorMessage: string;
private authStorageKey: string = 'tempAuth';
// private authStorageKey: string = 'tempAuth';
constructor(
private readonly mastodonService: MastodonWrapperService,
private readonly notificationService: NotificationService,
private readonly authService: AuthService,
private readonly store: Store,
private readonly activatedRoute: ActivatedRoute,
private readonly router: Router) {
// constructor(
// private readonly mastodonService: MastodonWrapperService,
// private readonly notificationService: NotificationService,
// private readonly authService: AuthService,
// private readonly store: Store,
// private readonly activatedRoute: ActivatedRoute,
// private readonly router: Router) {
this.activatedRoute.queryParams.subscribe(params => {
this.hasError = false;
// this.activatedRoute.queryParams.subscribe(params => {
// this.hasError = false;
const code = params['code'];
if (!code) {
this.displayError(RegistrationErrorTypes.CodeNotFound);
return;
}
// const code = params['code'];
// if (!code) {
// this.displayError(RegistrationErrorTypes.CodeNotFound);
// return;
// }
const appDataWrapper = <CurrentAuthProcess>JSON.parse(localStorage.getItem(this.authStorageKey));
if (!appDataWrapper) {
this.displayError(RegistrationErrorTypes.AuthProcessNotFound);
return;
}
// const appDataWrapper = <CurrentAuthProcess>JSON.parse(localStorage.getItem(this.authStorageKey));
// if (!appDataWrapper) {
// this.displayError(RegistrationErrorTypes.AuthProcessNotFound);
// return;
// }
const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0];
let usedTokenData: TokenData;
this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
.then((tokenData: TokenData) => {
// const appInfo = this.getAllSavedApps().filter(x => x.instance === appDataWrapper.instance)[0];
// let usedTokenData: TokenData;
// this.authService.getToken(appDataWrapper.instance, appInfo.app.client_id, appInfo.app.client_secret, code, appInfo.app.redirect_uri)
// .then((tokenData: TokenData) => {
if(tokenData.refresh_token && !tokenData.created_at){
const nowEpoch = Date.now() / 1000 | 0;
tokenData.created_at = nowEpoch;
}
// if(tokenData.refresh_token && !tokenData.created_at){
// const nowEpoch = Date.now() / 1000 | 0;
// tokenData.created_at = nowEpoch;
// }
usedTokenData = tokenData;
// usedTokenData = tokenData;
return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
})
.then((account: Account) => {
var username = account.username.toLowerCase();
var instance = appDataWrapper.instance.toLowerCase();
// return this.mastodonService.retrieveAccountDetails({ 'instance': appDataWrapper.instance, 'id': '', 'username': '', 'order': 0, 'isSelected': true, 'token': tokenData });
// })
// .then((account: Account) => {
// var username = account.username.toLowerCase();
// var instance = appDataWrapper.instance.toLowerCase();
if(this.isAccountAlreadyPresent(username, instance)){
this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
this.router.navigate(['/home']);
return;
}
// if(this.isAccountAlreadyPresent(username, instance)){
// this.notificationService.notify(null, null, `Account @${username}@${instance} is already registered`, true);
// this.router.navigate(['/home']);
// return;
// }
const accountInfo = new AccountInfo();
accountInfo.username = username;
accountInfo.instance = instance;
accountInfo.token = usedTokenData;
// const accountInfo = new AccountInfo();
// accountInfo.username = username;
// accountInfo.instance = instance;
// accountInfo.token = usedTokenData;
this.store.dispatch([new AddAccount(accountInfo)])
.subscribe(() => {
localStorage.removeItem(this.authStorageKey);
this.router.navigate(['/home']);
});
})
.catch((err: HttpErrorResponse) => {
this.notificationService.notifyHttpError(err, null);
});
});
}
// this.store.dispatch([new AddAccount(accountInfo)])
// .subscribe(() => {
// localStorage.removeItem(this.authStorageKey);
// this.router.navigate(['/home']);
// });
// })
// .catch((err: HttpErrorResponse) => {
// this.notificationService.notifyHttpError(err, null);
// });
// });
// }
ngOnInit() {
}
// ngOnInit() {
// }
private isAccountAlreadyPresent(username: string, instance: string): boolean{
const accounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
for (let acc of accounts) {
if(acc.instance === instance && acc.username == username){
return true;
}
}
return false;
}
// private isAccountAlreadyPresent(username: string, instance: string): boolean{
// const accounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
// for (let acc of accounts) {
// if(acc.instance === instance && acc.username == username){
// return true;
// }
// }
// return false;
// }
private displayError(type: RegistrationErrorTypes) {
this.hasError = true;
switch (type) {
case RegistrationErrorTypes.AuthProcessNotFound:
this.errorMessage = 'Something when wrong in the authentication process. Please retry.'
break;
case RegistrationErrorTypes.CodeNotFound:
this.errorMessage = 'No authentication code returned. Please retry.'
break;
}
}
// private displayError(type: RegistrationErrorTypes) {
// this.hasError = true;
// switch (type) {
// case RegistrationErrorTypes.AuthProcessNotFound:
// this.errorMessage = 'Something when wrong in the authentication process. Please retry.'
// break;
// case RegistrationErrorTypes.CodeNotFound:
// this.errorMessage = 'No authentication code returned. Please retry.'
// break;
// }
// }
private getAllSavedApps(): AppInfo[] {
const snapshot = <RegisteredAppsStateModel>this.store.snapshot().registeredapps;
return snapshot.apps;
}
}
// private getAllSavedApps(): AppInfo[] {
// const snapshot = <RegisteredAppsStateModel>this.store.snapshot().registeredapps;
// return snapshot.apps;
// }
// }
enum RegistrationErrorTypes {
CodeNotFound,
AuthProcessNotFound
}
// enum RegistrationErrorTypes {
// CodeNotFound,
// AuthProcessNotFound
// }

View File

@ -1,5 +1,4 @@
import { Component, OnInit, OnDestroy, QueryList, ViewChildren, ElementRef } from "@angular/core";
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subscription } from "rxjs";
import { Select } from "@ngxs/store";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
@ -20,20 +19,10 @@ export class StreamsMainDisplayComponent implements OnInit, OnDestroy {
private columnSelectedSub: Subscription;
constructor(
private readonly router: Router,
private readonly activatedRoute: ActivatedRoute,
private readonly navigationService: NavigationService) {
}
ngOnInit() {
this.activatedRoute.queryParams.subscribe(params => {
const code = params['code'];
if (code) {
this.router.navigate(['/register'], { queryParams: { code: code} });
return;
}
});
this.columnSelectedSub = this.navigationService.columnSelectedSubject.subscribe((columnIndex: number) => {
this.focusOnColumn(columnIndex);
});
@ -55,10 +44,6 @@ export class StreamsMainDisplayComponent implements OnInit, OnDestroy {
.then(() => {
this.streamComponents.toArray()[columnIndex].focus();
});
// setTimeout(() => {
// this.streamComponents.toArray()[columnIndex].focus();
// }, 500);
}, 0);

View File

@ -22,6 +22,9 @@ export class MastodonWrapperService {
let isExpired = false;
let storedAccountInfo = this.getStoreAccountInfo(accountInfo.id);
if(!storedAccountInfo || !(storedAccountInfo.token))
return Promise.resolve(accountInfo);
try {
if (storedAccountInfo.token.refresh_token) {
if (!storedAccountInfo.token.created_at || !storedAccountInfo.token.expires_in) {
@ -31,14 +34,12 @@ export class MastodonWrapperService {
//Pleroma workaround
let expire_in = storedAccountInfo.token.expires_in;
if(expire_in < 3600) {
if (expire_in < 3600) {
expire_in = 3600;
}
let expire_on = expire_in + storedAccountInfo.token.created_at;
isExpired = expire_on - nowEpoch <= 60 * 2;
//console.warn(`expiring in ${Math.round((expire_on - nowEpoch)/24/60/60)}days ${Math.round((expire_on - nowEpoch)/60/60)}h ${Math.round((expire_on - nowEpoch)/60)} mins`);
}
}
} catch (err) {
@ -46,10 +47,8 @@ export class MastodonWrapperService {
}
if (storedAccountInfo.token.refresh_token && isExpired) {
console.warn('--------------------------');
console.warn('-------->> MARTY!! -------');
console.warn('-------->> RENEW TOKEN FFS');
console.warn('--------------------------');
console.log('>>> MARTY!! ------------');
console.log('>>> RENEW TOKEN FFS ----');
const app = this.getAllSavedApps().find(x => x.instance === storedAccountInfo.instance);
return this.authService.refreshToken(storedAccountInfo.instance, app.app.client_id, app.app.client_secret, storedAccountInfo.token.refresh_token)
@ -87,7 +86,10 @@ export class MastodonWrapperService {
}
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
return this.mastodonService.retrieveAccountDetails(account);
return this.refreshAccountIfNeeded(account)
.then((refreshedAccount: AccountInfo) => {
return this.mastodonService.retrieveAccountDetails(refreshedAccount);
});
}
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20, tag: string = null, listId: string = null): Promise<Status[]> {

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ServiceWorkerService } from './service-worker.service';
xdescribe('ServiceWorkerService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ServiceWorkerService = TestBed.get(ServiceWorkerService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,52 @@
import { Injectable, ApplicationRef } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { first } from 'rxjs/operators';
import { interval, concat, BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ServiceWorkerService {
newAppVersionIsAvailable = new BehaviorSubject<boolean>(false);
private isListening = false;
constructor(appRef: ApplicationRef, private updates: SwUpdate) {
//https://angular.io/guide/service-worker-communications
updates.available.subscribe(event => {
console.log('current version is', event.current);
console.log('available version is', event.available);
this.newAppVersionIsAvailable.next(true);
});
// Allow the app to stabilize first, before starting polling for updates with `interval()`.
// const updateCheckTimer$ = interval(10 * 1000);
// const appIsStable$ = appRef.isStable; //.pipe(first(isStable => isStable === true));
// const everySixHoursOnceAppIsStable$ = concat(appIsStable$, updateCheckTimer$);
// everySixHoursOnceAppIsStable$.subscribe(() => {
// updates.checkForUpdate();
// });
const updateCheckTimer$ = interval(6 * 60 * 60 * 1000);
appRef.isStable.subscribe(() => {
if (this.isListening) return;
this.isListening = true;
updateCheckTimer$.subscribe(() => {
updates.checkForUpdate();
});
});
}
loadNewAppVersion() {
document.location.reload();
}
checkForUpdates(): Promise<void> {
return this.updates.checkForUpdate();
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

View File

@ -0,0 +1,48 @@
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Regular/Roboto-Regular.woff2') format('woff2'),
url('assets/fonts/Regular/Roboto-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Medium/Roboto-Medium.woff2') format('woff2'),
url('assets/fonts/Medium/Roboto-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Black/Roboto-Black.woff2') format('woff2'),
url('assets/fonts/Black/Roboto-Black.woff') format('woff');
font-weight: 900;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Thin/Roboto-Thin.woff2') format('woff2'),
url('assets/fonts/Thin/Roboto-Thin.woff') format('woff');
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Light/Roboto-Light.woff2') format('woff2'),
url('assets/fonts/Light/Roboto-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('assets/fonts/Bold/Roboto-Bold.woff2') format('woff2'),
url('assets/fonts/Bold/Roboto-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
}

View File

@ -8,8 +8,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> -->
<style>
.lds-ripple {
@ -50,6 +48,8 @@
}
}
</style>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#1976d2">
</head>
<body ondragstart="return false;" ondrop="return false;">
@ -59,5 +59,6 @@
<div></div>
</div>
</app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>

View File

@ -5,8 +5,13 @@ import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
.then(() => {
if ('serviceWorker' in navigator && environment.production) {
navigator.serviceWorker.register('./ngsw-worker.js');
}
})
.catch(err => console.log(err));

51
src/manifest.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "sengi",
"short_name": "sengi",
"theme_color": "#1976d2",
"background_color": "#fafafa",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "assets/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "assets/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "assets/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

27
src/ngsw-config.json Normal file
View File

@ -0,0 +1,27 @@
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/*.css",
"/*.js"
]
}
}, {
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}

View File

@ -4,6 +4,8 @@
@import "~bootstrap/scss/bootstrap.scss";
@import "~ng-pick-datetime/assets/style/picker.min.css";
@import 'assets/scss/modules/_fonts.scss';
*,
*::after,
*::before {