mirror of
https://github.com/NicolasConstant/sengi
synced 2025-02-09 08:28:40 +01:00
Merge pull request #59 from NicolasConstant/develop
Merge for Release 0.4
This commit is contained in:
commit
829b526d8a
@ -1,4 +1,4 @@
|
||||
[![AppVeyor master](https://img.shields.io/appveyor/ci/NicolasConstant/sengi/master.svg?style=flat-square)](https://ci.appveyor.com/project/NicolasConstant/sengi) [![AppVeyor tests master](https://img.shields.io/appveyor/tests/nicolasconstant/sengi/master.svg?style=flat-square)](https://ci.appveyor.com/project/NicolasConstant/sengi/build/tests) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/agpl-3.0)
|
||||
![GitHub release](https://img.shields.io/github/release/nicolasconstant/sengi.svg?style=flat-square) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg?style=flat-square)](https://www.gnu.org/licenses/agpl-3.0) [![AppVeyor master](https://img.shields.io/appveyor/ci/NicolasConstant/sengi/master.svg?style=flat-square)](https://ci.appveyor.com/project/NicolasConstant/sengi) [![AppVeyor tests master](https://img.shields.io/appveyor/tests/nicolasconstant/sengi/master.svg?style=flat-square)](https://ci.appveyor.com/project/NicolasConstant/sengi/build/tests)
|
||||
|
||||
## Introduction
|
||||
|
||||
@ -40,9 +40,9 @@ This project is licensed under the AGPLv3 License - see [LICENSE](LICENSE) for d
|
||||
|
||||
## Dependencies
|
||||
|
||||
* [Angular 6](https://github.com/angular/angular)
|
||||
* [Angular 7](https://github.com/angular/angular)
|
||||
* [NGXS](https://github.com/ngxs/store)
|
||||
* [SASS](https://github.com/sass/dart-sass)
|
||||
* [Electron](https://github.com/electron/electron)
|
||||
* [Electron 4](https://github.com/electron/electron)
|
||||
|
||||
|
||||
|
2
main.js
2
main.js
@ -95,6 +95,8 @@ function getFile(filePath, res, page404) {
|
||||
});
|
||||
};
|
||||
|
||||
app.commandLine.appendSwitch('force-color-profile', 'srgb');
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
10047
package-lock.json
generated
10047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@ -16,15 +16,15 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^6.1.0",
|
||||
"@angular/common": "^6.1.0",
|
||||
"@angular/compiler": "^6.1.0",
|
||||
"@angular/core": "^6.1.0",
|
||||
"@angular/forms": "^6.1.0",
|
||||
"@angular/http": "^6.1.0",
|
||||
"@angular/platform-browser": "^6.1.0",
|
||||
"@angular/platform-browser-dynamic": "^6.1.0",
|
||||
"@angular/router": "^6.1.0",
|
||||
"@angular/animations": "^7.2.7",
|
||||
"@angular/common": "^7.2.7",
|
||||
"@angular/compiler": "^7.2.7",
|
||||
"@angular/core": "^7.2.7",
|
||||
"@angular/forms": "^7.2.7",
|
||||
"@angular/http": "^7.2.7",
|
||||
"@angular/platform-browser": "^7.2.7",
|
||||
"@angular/platform-browser-dynamic": "^7.2.7",
|
||||
"@angular/router": "^7.2.7",
|
||||
"@fortawesome/angular-fontawesome": "^0.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.13",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.7.0",
|
||||
@ -34,21 +34,21 @@
|
||||
"@ngxs/store": "^3.2.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"core-js": "^2.5.4",
|
||||
"electron": "^4.0.6",
|
||||
"ionicons": "^4.4.3",
|
||||
"ngx-electron": "^1.0.4",
|
||||
"rxjs": "^6.0.0",
|
||||
"zone.js": "^0.8.26"
|
||||
"rxjs": "^6.4.0",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "^0.8.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.7.0",
|
||||
"@angular/cli": "~6.1.1",
|
||||
"@angular/compiler-cli": "^6.1.0",
|
||||
"@angular/language-service": "^6.1.0",
|
||||
"@angular-devkit/build-angular": "~0.13.0",
|
||||
"@angular/cli": "~7.3.3",
|
||||
"@angular/compiler-cli": "^7.2.7",
|
||||
"@angular/language-service": "^7.2.7",
|
||||
"@types/jasmine": "~2.8.6",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"codelyzer": "~4.2.1",
|
||||
"electron": "^2.0.5",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~1.7.1",
|
||||
@ -60,6 +60,6 @@
|
||||
"protractor": "~5.3.0",
|
||||
"ts-node": "~5.0.1",
|
||||
"tslint": "~5.9.1",
|
||||
"typescript": "~2.7.2"
|
||||
"typescript": "~3.2.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,35 @@
|
||||
<app-media-viewer
|
||||
id="media-viewer"
|
||||
*ngIf="openedMediaEvent"
|
||||
[openedMediaEvent]="openedMediaEvent" (closeSubject)="closeMedia()"></app-media-viewer>
|
||||
|
||||
<app-left-side-bar>
|
||||
</app-left-side-bar>
|
||||
|
||||
<!--<app-streams-main-display>
|
||||
</app-streams-main-display>-->
|
||||
|
||||
<div id="display-zone">
|
||||
<app-tutorial id="tutorial" *ngIf="tutorialActive"></app-tutorial>
|
||||
<app-floating-column id="floating-column" *ngIf="floatingColumnActive"></app-floating-column>
|
||||
<app-notification-hub></app-notification-hub>
|
||||
<router-outlet></router-outlet>
|
||||
<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">
|
||||
<div class="drag-and-drop__border">
|
||||
<div class="drag-and-drop__label">
|
||||
Drag & drop to upload
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-streams-selection-footer>
|
||||
</app-streams-selection-footer>
|
||||
<app-media-viewer id="media-viewer" *ngIf="openedMediaEvent" [openedMediaEvent]="openedMediaEvent"
|
||||
(closeSubject)="closeMedia()" (dragenter)="dragenter($event)"></app-media-viewer>
|
||||
|
||||
<div class="app" (dragenter)="dragenter($event)">
|
||||
<app-left-side-bar>
|
||||
</app-left-side-bar>
|
||||
|
||||
<!--<app-streams-main-display>
|
||||
</app-streams-main-display>-->
|
||||
|
||||
<div id="display-zone">
|
||||
<app-tutorial id="tutorial" *ngIf="tutorialActive"></app-tutorial>
|
||||
<app-floating-column id="floating-column" *ngIf="floatingColumnActive"></app-floating-column>
|
||||
<app-notification-hub></app-notification-hub>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
<app-streams-selection-footer>
|
||||
</app-streams-selection-footer>
|
||||
</div>
|
||||
|
||||
<!--<div style="text-align:center">
|
||||
<h1>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import"variables";
|
||||
|
||||
#display-zone {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -18,7 +20,7 @@
|
||||
|
||||
#tutorial {
|
||||
position: relative;
|
||||
top: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
@ -30,7 +32,63 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.app {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.drag-and-drop {
|
||||
transition: all .2s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99999999;
|
||||
background: rgba(black, .8);
|
||||
// opacity: 0.2;
|
||||
&__on-drag {
|
||||
opacity: 1;
|
||||
// background: rgba(black, .7);
|
||||
}
|
||||
&__card {
|
||||
background: $font-link-primary;
|
||||
background: $column-color;
|
||||
max-width: 300px;
|
||||
|
||||
$card-height: 200px;
|
||||
height: $card-height;
|
||||
position: relative;
|
||||
top: calc(50% - #{$card-height}/2);
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
|
||||
border-radius: 5px;
|
||||
// transform: perspective(1px) translateY(-50%);
|
||||
|
||||
// margin-top: calc(50%);
|
||||
// transform: translateY(50px);
|
||||
}
|
||||
&__border {
|
||||
border: 3px black dashed;
|
||||
// margin: 50px 50px 0 50px;
|
||||
height: calc(100%);
|
||||
}
|
||||
&__label {
|
||||
font-size: 17px;
|
||||
// font-weight: 400;
|
||||
text-align: center;
|
||||
height: 100px;
|
||||
margin: auto;
|
||||
padding-top: 70px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
app-streams-selection-footer {
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { Subscription, Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, map } from 'rxjs/operators';
|
||||
import { Select } from '@ngxs/store';
|
||||
// import { ElectronService } from 'ngx-electron';
|
||||
|
||||
import { NavigationService, LeftPanelType } from './services/navigation.service';
|
||||
import { StreamElement } from './states/streams.state';
|
||||
import { OpenMediaEvent } from './models/common.model';
|
||||
import { ToolsService } from './services/tools.service';
|
||||
import { MediaService } from './services/media.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -22,10 +25,14 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private columnEditorSub: Subscription;
|
||||
private openMediaSub: Subscription;
|
||||
private streamSub: Subscription;
|
||||
private dragoverSub: Subscription;
|
||||
|
||||
@Select(state => state.streamsstatemodel.streams) streamElements$: Observable<StreamElement[]>;
|
||||
|
||||
constructor(private readonly navigationService: NavigationService) {
|
||||
constructor(
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mediaService: MediaService,
|
||||
private readonly navigationService: NavigationService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -49,20 +56,61 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
if (openedMediaEvent) {
|
||||
this.openedMediaEvent = openedMediaEvent;
|
||||
// this.mediaViewerActive = true;
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.dragoverSub = this.dragoverSubject
|
||||
.pipe(
|
||||
debounceTime(150)
|
||||
)
|
||||
.subscribe(() => {
|
||||
console.warn('disable drag');
|
||||
this.drag = false;
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.streamSub.unsubscribe();
|
||||
this.columnEditorSub.unsubscribe();
|
||||
this.openMediaSub.unsubscribe();
|
||||
this.dragoverSub.unsubscribe();
|
||||
}
|
||||
|
||||
closeMedia(){
|
||||
console.warn('closeMedia()');
|
||||
closeMedia() {
|
||||
this.openedMediaEvent = null;
|
||||
}
|
||||
|
||||
private dragoverSubject = new Subject<boolean>();
|
||||
drag: boolean;
|
||||
dragenter(event): boolean {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.drag = true;
|
||||
return false;
|
||||
}
|
||||
dragleave(event): boolean {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.drag = false;
|
||||
return false;
|
||||
}
|
||||
dragover(event): boolean {
|
||||
// console.warn('dragover');
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.dragoverSubject.next(true);
|
||||
return false;
|
||||
}
|
||||
drop(event): boolean {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.drag = false;
|
||||
|
||||
let files = <File[]>event.dataTransfer.files;
|
||||
const selectedAccount = this.toolsService.getSelectedAccounts()[0];
|
||||
this.mediaService.uploadMedia(selectedAccount, files);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { NgxElectronModule } from "ngx-electron";
|
||||
// import { NgxElectronModule } from "ngx-electron";
|
||||
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
|
||||
@ -36,7 +36,6 @@ import { AddNewStatusComponent } from "./components/floating-column/add-new-stat
|
||||
import { ManageAccountComponent } from "./components/floating-column/manage-account/manage-account.component";
|
||||
import { ActionBarComponent } from './components/stream/status/action-bar/action-bar.component';
|
||||
import { WaitingAnimationComponent } from './components/waiting-animation/waiting-animation.component';
|
||||
import { ReplyToStatusComponent } from './components/stream/status/reply-to-status/reply-to-status.component';
|
||||
import { UserProfileComponent } from './components/stream/user-profile/user-profile.component';
|
||||
import { ThreadComponent } from './components/stream/thread/thread.component';
|
||||
import { HashtagComponent } from './components/stream/hashtag/hashtag.component';
|
||||
@ -49,6 +48,8 @@ import { TutorialComponent } from './components/tutorial/tutorial.component';
|
||||
import { NotificationHubComponent } from './components/notification-hub/notification-hub.component';
|
||||
import { NotificationService } from "./services/notification.service";
|
||||
import { MediaViewerComponent } from './components/media-viewer/media-viewer.component';
|
||||
import { CreateStatusComponent } from './components/create-status/create-status.component';
|
||||
import { MediaComponent } from './components/create-status/media/media.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
@ -76,7 +77,6 @@ const routes: Routes = [
|
||||
SearchComponent,
|
||||
ActionBarComponent,
|
||||
WaitingAnimationComponent,
|
||||
ReplyToStatusComponent,
|
||||
UserProfileComponent,
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
@ -87,7 +87,9 @@ const routes: Routes = [
|
||||
StreamEditionComponent,
|
||||
TutorialComponent,
|
||||
NotificationHubComponent,
|
||||
MediaViewerComponent
|
||||
MediaViewerComponent,
|
||||
CreateStatusComponent,
|
||||
MediaComponent
|
||||
],
|
||||
imports: [
|
||||
FontAwesomeModule,
|
||||
@ -95,7 +97,6 @@ const routes: Routes = [
|
||||
HttpModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
NgxElectronModule,
|
||||
RouterModule.forRoot(routes),
|
||||
|
||||
NgxsModule.forRoot([
|
||||
|
@ -0,0 +1,28 @@
|
||||
<form class="status-form" (ngSubmit)="onSubmit()">
|
||||
<div class="status-form__sending" *ngIf="isSending">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off"
|
||||
placeholder="Title, Content Warning (optional)" title="title, content warning (optional)" />
|
||||
|
||||
<textarea #reply [(ngModel)]="status" name="status"
|
||||
class="form-control form-control-sm status-form__status flexcroll" rows="5" required title="content"
|
||||
placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
|
||||
|
||||
<div class="status-form__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to the
|
||||
start in order to use multiposting.</div>
|
||||
|
||||
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy"
|
||||
[(ngModel)]="selectedPrivacy">
|
||||
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
|
||||
</select>
|
||||
<div class="status-form__counter">
|
||||
<span class="status-form__counter--count">{{charCountLeft}}</span> <span
|
||||
class="status-form__counter--posts">{{postCounts - 1}}/{{postCounts}}</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="statusReplyingToWrapper">REPLY!</button>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary" *ngIf="!statusReplyingToWrapper">POST!</button>
|
||||
|
||||
<app-media></app-media>
|
||||
</form>
|
@ -0,0 +1,97 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
$btn-send-status-width: 60px;
|
||||
$counter-width: 90px;
|
||||
.form-control {
|
||||
margin: 0 0 5px 5px;
|
||||
width: calc(100% - 10px);
|
||||
background-color: $column-color;
|
||||
border-color: $status-secondary-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
&--privacy {
|
||||
display: inline-block;
|
||||
width: calc(100% - 15px - #{$btn-send-status-width} - #{$counter-width});
|
||||
}
|
||||
}
|
||||
|
||||
.btn-custom-primary {
|
||||
display: inline-block;
|
||||
width: $btn-send-status-width;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px; // background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500; // &:hover {
|
||||
// }
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
}
|
||||
|
||||
.status-form {
|
||||
position: relative;
|
||||
font-size: $default-font-size;
|
||||
&__sending {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
background-color: rgba($column-color, .75);
|
||||
z-index: 2;
|
||||
&--waiting {
|
||||
margin-top: calc(25%);
|
||||
}
|
||||
}
|
||||
&__counter {
|
||||
display: inline-block;
|
||||
border: 1px solid $status-secondary-color;
|
||||
margin-left: 5px;
|
||||
width: calc(#{$counter-width} - 5px);
|
||||
height: 32px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
padding: 4px 7px 0 7px; // color: lighten($font-link-primary-hover, 10);
|
||||
// position: relative;
|
||||
// overflow: hidden;
|
||||
&--count {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// overflow: hidden;
|
||||
// outline: 1px solid greenyellow;
|
||||
}
|
||||
&--posts {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
&__status {
|
||||
&::-webkit-resizer {
|
||||
// border: 2px solid black;
|
||||
background: $font-link-primary-hover;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
// box-shadow: 0 0 5px 5px blue;
|
||||
// outline: 2px solid yellow;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__mention-error {
|
||||
border: 2px dashed red;
|
||||
padding: 5px 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
135
src/app/components/create-status/create-status.component.spec.ts
Normal file
135
src/app/components/create-status/create-status.component.spec.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgxsModule } from '@ngxs/store';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { CreateStatusComponent } from './create-status.component';
|
||||
import { WaitingAnimationComponent } from '../waiting-animation/waiting-animation.component';
|
||||
import { MediaComponent } from './media/media.component';
|
||||
import { RegisteredAppsState } from '../../states/registered-apps.state';
|
||||
import { AccountsState } from '../../states/accounts.state';
|
||||
import { StreamsState } from '../../states/streams.state';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { MastodonService } from '../../services/mastodon.service';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('CreateStatusComponent', () => {
|
||||
let component: CreateStatusComponent;
|
||||
let fixture: ComponentFixture<CreateStatusComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
//this.component = new CreateStatusComponent(null, null, null, null, null);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CreateStatusComponent, WaitingAnimationComponent, MediaComponent],
|
||||
imports: [
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
NgxsModule.forRoot([
|
||||
RegisteredAppsState,
|
||||
AccountsState,
|
||||
StreamsState
|
||||
]),
|
||||
],
|
||||
providers: [NavigationService, NotificationService, MastodonService],
|
||||
schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateStatusComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not parse small status', () => {
|
||||
const status = 'this is a cool status';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe(status);
|
||||
});
|
||||
|
||||
it('should parse small status in two', () => {
|
||||
const status = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar.';
|
||||
(<any>component).maxCharLength = 66;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit. (...)');
|
||||
expect(result[1]).toBe('Mauris sed ante id dolor vulputate pulvinar.');
|
||||
});
|
||||
|
||||
it('should parse medium status in two', () => {
|
||||
const status = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictumst. Cras ut mauris vitae est finibus faucibus vitae sed arcu. Praesent sem nisl, accumsan sed fringilla at, viverra nec felis. Morbi sit amet diam in quam mollis aliquet. Integer finibus nunc nunc. Suspendisse quam nisl, condimentum vitae lacus sed, lacinia dictum tellus. Mauris vitae odio ac leo bibendum facilisis.';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea (...)');
|
||||
expect(result[1]).toBe('dictumst. Cras ut mauris vitae est finibus faucibus vitae sed arcu. Praesent sem nisl, accumsan sed fringilla at, viverra nec felis. Morbi sit amet diam in quam mollis aliquet. Integer finibus nunc nunc. Suspendisse quam nisl, condimentum vitae lacus sed, lacinia dictum tellus. Mauris vitae odio ac leo bibendum facilisis.');
|
||||
expect(result[0].length).toBeLessThanOrEqual(500);
|
||||
expect(result[1].length).toBeLessThanOrEqual(500);
|
||||
});
|
||||
|
||||
it('should not parse exact status in two', () => {
|
||||
const status = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictum';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should parse big status in three', () => {
|
||||
const status = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lobortis dui vitae libero ultricies, eget lobortis velit finibus. Curabitur et finibus diam, quis facilisis nisi. In dapibus, orci vel posuere consectetur, nunc nisl interdum quam, a commodo neque arcu vitae neque. Vivamus porta, diam nec sollicitudin interdum, lectus diam pulvinar nulla, in ornare tellus odio et turpis. Nam et ipsum id mauris suscipit tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec nec ullamcorper mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc venenatis ipsum et felis ultrices porta. Sed justo nisl, sollicitudin sed nisi id, malesuada posuere lacus. Morbi condimentum tincidunt porta. Nunc vestibulum tellus sit amet quam sagittis, ac sollicitudin mi ullamcorper. Nunc eget sapien blandit purus convallis pellentesque. Fusce feugiat eu lacus vitae mattis. Vestibulum rhoncus nulla eu consectetur mollis. Phasellus venenatis at ligula eu feugiat. Donec ultricies ante fringilla, aliquet purus sit amet, rutrum justo. Maecenas sit amet magna laoreet, fermentum lectus nec, pretium ligula. Aenean gravida dolor vitae nibh sodales, vitae consectetur nibh dapibus. Mauris viverra congue ornare. Etiam quis mi fringilla lacus.';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0]).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam lobortis dui vitae libero ultricies, eget lobortis velit finibus. Curabitur et finibus diam, quis facilisis nisi. In dapibus, orci vel posuere consectetur, nunc nisl interdum quam, a commodo neque arcu vitae neque. Vivamus porta, diam nec sollicitudin interdum, lectus diam pulvinar nulla, in ornare tellus odio et turpis. Nam et ipsum id mauris suscipit tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia (...)');
|
||||
expect(result[1]).toBe('nostra, per inceptos himenaeos. Donec nec ullamcorper mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc venenatis ipsum et felis ultrices porta. Sed justo nisl, sollicitudin sed nisi id, malesuada posuere lacus. Morbi condimentum tincidunt porta. Nunc vestibulum tellus sit amet quam sagittis, ac sollicitudin mi ullamcorper. Nunc eget sapien blandit purus convallis pellentesque. Fusce feugiat eu lacus vitae mattis. Vestibulum rhoncus nulla eu consectetur mollis. (...)');
|
||||
expect(result[2]).toBe('Phasellus venenatis at ligula eu feugiat. Donec ultricies ante fringilla, aliquet purus sit amet, rutrum justo. Maecenas sit amet magna laoreet, fermentum lectus nec, pretium ligula. Aenean gravida dolor vitae nibh sodales, vitae consectetur nibh dapibus. Mauris viverra congue ornare. Etiam quis mi fringilla lacus.');
|
||||
expect(result[0].length).toBeLessThanOrEqual(500);
|
||||
expect(result[1].length).toBeLessThanOrEqual(500);
|
||||
expect(result[2].length).toBeLessThanOrEqual(500);
|
||||
});
|
||||
|
||||
it('should not count domain length when replying', () => {
|
||||
const status = '@Lorem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not count domain length when replying', () => {
|
||||
const status = '@Lorem@ipsum.com @1orem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse plate';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should add alias in multiposting replies', () => {
|
||||
const status = '@Lorem@ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu0';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].length).toBeLessThanOrEqual(510);
|
||||
expect(result[1].length).toBeLessThanOrEqual(510);
|
||||
expect(result[0]).toContain('@Lorem@ipsum.com ');
|
||||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||
});
|
||||
|
||||
it('should add alias in multiposting replies', () => {
|
||||
const status = '@Lorem@ipsum.com @48756@987586.ipsum.com ipsum dolor sit amet, consectetur adipiscing elit. Mauris sed ante id dolor vulputate pulvinar sit amet a nisl. Duis sagittis nisl sit amet est rhoncus rutrum. Duis aliquet eget erat nec molestie. Fusce bibendum consectetur rhoncus. Aenean vel neque ac diam hendrerit interdum id a nisl. Aenean leo ante, luctus eget erat at, interdum tincidunt turpis. Donec non efficitur magna. Nam placerat convallis tincidunt. Etiam ac scelerisque velit, at vestibulum turpis. In hac habitasse platea dictu0';
|
||||
(<any>component).maxCharLength = 500;
|
||||
const result = <string[]>(<any>component).parseStatus(status);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].length).toBeLessThanOrEqual(527);
|
||||
expect(result[1].length).toBeLessThanOrEqual(527);
|
||||
expect(result[0]).toContain('@Lorem@ipsum.com ');
|
||||
expect(result[1]).toContain('@Lorem@ipsum.com ');
|
||||
console.warn(result);
|
||||
});
|
||||
|
||||
});
|
311
src/app/components/create-status/create-status.component.ts
Normal file
311
src/app/components/create-status/create-status.component.ts
Normal file
@ -0,0 +1,311 @@
|
||||
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
|
||||
import { MastodonService, VisibilityEnum } from '../../services/mastodon.service';
|
||||
import { Status, Attachment } from '../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../services/tools.service';
|
||||
import { NotificationService } from '../../services/notification.service';
|
||||
import { StatusWrapper } from '../../models/common.model';
|
||||
import { AccountInfo } from '../../states/accounts.state';
|
||||
import { InstancesInfoService } from '../../services/instances-info.service';
|
||||
import { MediaService } from '../../services/media.service';
|
||||
import { identifierModuleUrl } from '@angular/compiler';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-status',
|
||||
templateUrl: './create-status.component.html',
|
||||
styleUrls: ['./create-status.component.scss']
|
||||
})
|
||||
export class CreateStatusComponent implements OnInit, OnDestroy {
|
||||
title: string;
|
||||
|
||||
private _status: string = '';
|
||||
set status(value: string) {
|
||||
this.countStatusChar(value);
|
||||
this._status = value;
|
||||
}
|
||||
get status(): string {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
private maxCharLength: number;
|
||||
charCountLeft: number;
|
||||
postCounts: number = 1;
|
||||
|
||||
isSending: boolean;
|
||||
|
||||
@Input() statusReplyingToWrapper: StatusWrapper;
|
||||
@Output() onClose = new EventEmitter();
|
||||
@ViewChild('reply') replyElement: ElementRef;
|
||||
|
||||
private statusReplyingTo: Status;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonService,
|
||||
private readonly instancesInfoService: InstancesInfoService,
|
||||
private readonly mediaService: MediaService) {
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
this.accountChanged(accounts);
|
||||
});
|
||||
|
||||
if (this.statusReplyingToWrapper) {
|
||||
if (this.statusReplyingToWrapper.status.reblog) {
|
||||
this.statusReplyingTo = this.statusReplyingToWrapper.status.reblog;
|
||||
} else {
|
||||
this.statusReplyingTo = this.statusReplyingToWrapper.status;
|
||||
}
|
||||
|
||||
const uniqueMentions = this.getMentions(this.statusReplyingTo, this.statusReplyingToWrapper.provider);
|
||||
for (const mention of uniqueMentions) {
|
||||
this.status += `@${mention} `;
|
||||
}
|
||||
|
||||
this.title = this.statusReplyingTo.spoiler_text;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
private accountChanged(accounts: AccountInfo[]): void {
|
||||
if (accounts && accounts.length > 0) {
|
||||
const selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
this.instancesInfoService.getMaxStatusChars(selectedAccount.instance)
|
||||
.then((maxChars: number) => {
|
||||
this.maxCharLength = maxChars;
|
||||
this.countStatusChar(this.status);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
|
||||
this.instancesInfoService.getDefaultPrivacy(selectedAccount)
|
||||
.then((defaultPrivacy: VisibilityEnum) => {
|
||||
switch (defaultPrivacy) {
|
||||
case VisibilityEnum.Public:
|
||||
this.selectedPrivacy = 'Public';
|
||||
break;
|
||||
case VisibilityEnum.Unlisted:
|
||||
this.selectedPrivacy = 'Unlisted';
|
||||
break;
|
||||
case VisibilityEnum.Private:
|
||||
this.selectedPrivacy = 'Follows-only';
|
||||
break;
|
||||
case VisibilityEnum.Direct:
|
||||
this.selectedPrivacy = 'DM';
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mentionTooFarAwayError: boolean;
|
||||
|
||||
private countStatusChar(status: string) {
|
||||
this.mentionTooFarAwayError = false;
|
||||
const parseStatus = this.parseStatus(status);
|
||||
|
||||
const mentions = this.getMentionsFromStatus(status);
|
||||
if(mentions.length > 0){
|
||||
let containAllMention = true;
|
||||
mentions.forEach(m => {
|
||||
if(!parseStatus[0].includes(m)){
|
||||
containAllMention = false;
|
||||
}
|
||||
});
|
||||
|
||||
if(!containAllMention){
|
||||
this.mentionTooFarAwayError = true;
|
||||
this.charCountLeft = this.maxCharLength - status.length;
|
||||
this.postCounts = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// const lastMention = mentions[mentions.length - 1];
|
||||
// const lastMentionPosition = status.lastIndexOf(lastMention);
|
||||
// console.warn(`lastMentionPosition ${lastMentionPosition}`);
|
||||
// if(lastMentionPosition > (this.maxCharLength - lastMention.length * 2 + 10)){
|
||||
// this.mentionTooFarAwayError = true;
|
||||
// this.charCountLeft = this.maxCharLength - status.length;
|
||||
// this.postCounts = 1;
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
|
||||
const currentStatus = parseStatus[parseStatus.length - 1];
|
||||
const statusExtraChars = this.getMentionExtraChars(status);
|
||||
|
||||
const statusLength = currentStatus.length - statusExtraChars;
|
||||
this.charCountLeft = this.maxCharLength - statusLength;
|
||||
this.postCounts = parseStatus.length;
|
||||
}
|
||||
|
||||
private getMentions(status: Status, providerInfo: AccountInfo): string[] {
|
||||
const mentions = [...status.mentions.map(x => x.acct), status.account.acct];
|
||||
|
||||
let uniqueMentions = [];
|
||||
for (let mention of mentions) {
|
||||
if (!uniqueMentions.includes(mention)) {
|
||||
uniqueMentions.push(mention);
|
||||
}
|
||||
}
|
||||
|
||||
let globalUniqueMentions = [];
|
||||
for (let mention of uniqueMentions) {
|
||||
if (!mention.includes('@')) {
|
||||
mention += `@${providerInfo.instance}`;
|
||||
}
|
||||
globalUniqueMentions.push(mention);
|
||||
}
|
||||
|
||||
return globalUniqueMentions;
|
||||
}
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
this.onSubmit();
|
||||
return false;
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
if (this.isSending || this.mentionTooFarAwayError) return false;
|
||||
|
||||
this.isSending = true;
|
||||
|
||||
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
|
||||
switch (this.selectedPrivacy) { //FIXME: in case of responding, set the visibility to original
|
||||
case 'Public':
|
||||
visibility = VisibilityEnum.Public;
|
||||
break;
|
||||
case 'Unlisted':
|
||||
visibility = VisibilityEnum.Unlisted;
|
||||
break;
|
||||
case 'Follows-only':
|
||||
visibility = VisibilityEnum.Private;
|
||||
break;
|
||||
case 'DM':
|
||||
visibility = VisibilityEnum.Direct;
|
||||
break;
|
||||
}
|
||||
|
||||
const mediaAttachments = this.mediaService.mediaSubject.value.map(x => x.attachment);
|
||||
|
||||
const acc = this.toolsService.getSelectedAccounts()[0];
|
||||
let usableStatus: Promise<Status>;
|
||||
if (this.statusReplyingToWrapper) {
|
||||
usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
||||
} else {
|
||||
usableStatus = Promise.resolve(null);
|
||||
}
|
||||
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
return this.sendStatus(acc, this.status, visibility, this.title, status, mediaAttachments);
|
||||
})
|
||||
.then((res: Status) => {
|
||||
this.title = '';
|
||||
this.status = '';
|
||||
this.onClose.emit();
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
})
|
||||
.then(() => {
|
||||
this.isSending = false;
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sendStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, title: string, previousStatus: Status, attachments: Attachment[]): Promise<Status> {
|
||||
let parsedStatus = this.parseStatus(status);
|
||||
let resultPromise = Promise.resolve(previousStatus);
|
||||
|
||||
for (let i = 0; i < parsedStatus.length; i++) {
|
||||
let s = parsedStatus[i];
|
||||
resultPromise = resultPromise.then((pStatus: Status) => {
|
||||
let inReplyToId = null;
|
||||
if (pStatus) {
|
||||
inReplyToId = pStatus.id;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, attachments.map(x => x.id))
|
||||
.then((status: Status) => {
|
||||
this.mediaService.clearMedia();
|
||||
return status;
|
||||
});
|
||||
} else {
|
||||
return this.mastodonService.postNewStatus(account, s, visibility, title, inReplyToId, []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
private parseStatus(status: string): string[] {
|
||||
let mentionExtraChars = this.getMentionExtraChars(status);
|
||||
let trucatedStatus = `${status}`;
|
||||
let results = [];
|
||||
|
||||
let aggregateMention = '';
|
||||
let mentions = this.getMentionsFromStatus(status);
|
||||
mentions.forEach(x => {
|
||||
aggregateMention += `${x} `;
|
||||
});
|
||||
|
||||
const currentMaxCharLength = this.maxCharLength + mentionExtraChars;
|
||||
const maxChars = currentMaxCharLength - 6;
|
||||
|
||||
while (trucatedStatus.length > currentMaxCharLength) {
|
||||
const nextIndex = trucatedStatus.lastIndexOf(' ', maxChars);
|
||||
results.push(trucatedStatus.substr(0, nextIndex) + ' (...)');
|
||||
trucatedStatus = aggregateMention + trucatedStatus.substr(nextIndex + 1);
|
||||
}
|
||||
results.push(trucatedStatus);
|
||||
return results;
|
||||
}
|
||||
|
||||
private getMentionExtraChars(status: string): number{
|
||||
let mentionExtraChars = 0;
|
||||
let mentions = this.getMentionsFromStatus(status);
|
||||
|
||||
for (const mention of mentions) {
|
||||
if (mention.lastIndexOf('@') !== 0) {
|
||||
const domain = mention.split('@')[2];
|
||||
if (domain.length > 1) {
|
||||
mentionExtraChars += (domain.length + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mentionExtraChars;
|
||||
}
|
||||
|
||||
private getMentionsFromStatus(status: string): string[]{
|
||||
return status.split(' ').filter(x => x.indexOf('@') === 0 && x.length > 1);
|
||||
}
|
||||
}
|
19
src/app/components/create-status/media/media.component.html
Normal file
19
src/app/components/create-status/media/media.component.html
Normal file
@ -0,0 +1,19 @@
|
||||
<div *ngFor="let m of media" class="media">
|
||||
<div *ngIf="m.attachment === null" class="media__loading" title="{{m.file.name}}">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div *ngIf="m.attachment !== null" class="media__loaded" title="{{m.file.name}}"
|
||||
(mouseleave) ="updateMedia(m)">
|
||||
<div class="media__loaded--migrating" *ngIf="m.isMigrating">
|
||||
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
|
||||
</div>
|
||||
<div class="media__loaded--hover">
|
||||
<button class="media__loaded--button" title="remove" (click)="removeMedia(m)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</button>
|
||||
<input class="media__loaded--description" [(ngModel)]="m.description"
|
||||
autocomplete="off" placeholder="Describe for the visually impaired"/>
|
||||
</div>
|
||||
<img class="media__loaded--preview" src="{{m.attachment.preview_url}}" />
|
||||
</div>
|
||||
</div>
|
80
src/app/components/create-status/media/media.component.scss
Normal file
80
src/app/components/create-status/media/media.component.scss
Normal file
@ -0,0 +1,80 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
@import "mixins";
|
||||
|
||||
.media {
|
||||
width: calc(100%);
|
||||
padding: 0 5px 5px 5px;
|
||||
|
||||
&__loading{
|
||||
width: calc(100%);
|
||||
border: 1px solid $status-secondary-color;
|
||||
// background: rgb(0, 96, 134);
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
&__loaded{
|
||||
width: calc(100%);
|
||||
height: 75px;
|
||||
border: 1px solid $status-secondary-color;
|
||||
position: relative;
|
||||
transition: all .2s;
|
||||
|
||||
&--hover {
|
||||
position: absolute;
|
||||
top:0;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
background: rgba(black, .5);
|
||||
}
|
||||
|
||||
&--migrating{
|
||||
position: absolute;
|
||||
top:0;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
z-index: 20;
|
||||
opacity: 100;
|
||||
background: rgba(black, .7);
|
||||
}
|
||||
|
||||
&:hover &--hover {
|
||||
opacity: 100;
|
||||
}
|
||||
|
||||
&--button {
|
||||
@include clearButton;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
top:5px;
|
||||
right:8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&--description {
|
||||
position: absolute;
|
||||
bottom:5px;
|
||||
left: 5px;
|
||||
width: calc(100% - 10px);
|
||||
// background: black;
|
||||
// color: white;
|
||||
}
|
||||
|
||||
&--preview {
|
||||
// display: block;
|
||||
|
||||
width: calc(100%);
|
||||
height: calc(100%);
|
||||
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReplyToStatusComponent } from './reply-to-status.component';
|
||||
import { MediaComponent } from './media.component';
|
||||
|
||||
xdescribe('ReplyToStatusComponent', () => {
|
||||
let component: ReplyToStatusComponent;
|
||||
let fixture: ComponentFixture<ReplyToStatusComponent>;
|
||||
xdescribe('MediaComponent', () => {
|
||||
let component: MediaComponent;
|
||||
let fixture: ComponentFixture<MediaComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ReplyToStatusComponent ]
|
||||
declarations: [ MediaComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ReplyToStatusComponent);
|
||||
fixture = TestBed.createComponent(MediaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
57
src/app/components/create-status/media/media.component.ts
Normal file
57
src/app/components/create-status/media/media.component.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Subscription, Observable } from 'rxjs';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { MediaService, MediaWrapper } from '../../../services/media.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-media',
|
||||
templateUrl: './media.component.html',
|
||||
styleUrls: ['./media.component.scss']
|
||||
})
|
||||
export class MediaComponent implements OnInit, OnDestroy {
|
||||
faTimes = faTimes;
|
||||
media: MediaWrapper[] = [];
|
||||
private mediaSub: Subscription;
|
||||
|
||||
private accounts$: Observable<AccountInfo[]>;
|
||||
private accountSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mediaService: MediaService) {
|
||||
|
||||
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.mediaSub = this.mediaService.mediaSubject.subscribe((media: MediaWrapper[]) => {
|
||||
this.media = media;
|
||||
});
|
||||
|
||||
this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
|
||||
const selectedAccount = accounts.filter(x => x.isSelected)[0];
|
||||
this.mediaService.migrateMedias(selectedAccount);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mediaSub.unsubscribe();
|
||||
this.accountSub.unsubscribe();
|
||||
}
|
||||
|
||||
removeMedia(media: MediaWrapper): boolean {
|
||||
this.mediaService.remove(media);
|
||||
return false;
|
||||
}
|
||||
|
||||
updateMedia(media: MediaWrapper): boolean {
|
||||
const account = this.toolsService.getSelectedAccounts()[0];
|
||||
this.mediaService.update(account, media);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -8,5 +8,4 @@
|
||||
<br />
|
||||
<button type="submit" class="btn btn-success btn-sm">Submit</button>
|
||||
</form>
|
||||
|
||||
</div>
|
@ -34,7 +34,13 @@ export class AddNewAccountComponent implements OnInit {
|
||||
this.redirectToInstanceAuthPage(username, instance, appData);
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
} else if ((<Error>err).message === 'CORS'){
|
||||
this.notificationService.notify('Connection Error. It\'s usually a CORS issue with the server you\'re connecting to. Please check in the console and if so, contact your administrator with those informations.', true);
|
||||
} else {
|
||||
this.notificationService.notify('Unkown error', true);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
@ -53,6 +59,13 @@ export class AddNewAccountComponent implements OnInit {
|
||||
return this.saveNewApp(instance, appData)
|
||||
.then(() => { return appData; });
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
if (err.status === 0) {
|
||||
throw Error('CORS');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +73,7 @@ export class AddNewAccountComponent implements OnInit {
|
||||
const snapshot = <RegisteredAppsStateModel>this.store.snapshot().registeredapps;
|
||||
return snapshot.apps;
|
||||
}
|
||||
|
||||
|
||||
private redirectToInstanceAuthPage(username: string, instance: string, app: AppData) {
|
||||
const appDataTemp = new CurrentAuthProcess(username, instance);
|
||||
localStorage.setItem('tempAuth', JSON.stringify(appDataTemp));
|
||||
@ -69,11 +82,11 @@ export class AddNewAccountComponent implements OnInit {
|
||||
|
||||
window.location.href = instanceUrl;
|
||||
}
|
||||
|
||||
|
||||
private getLocalHostname(): string {
|
||||
let localHostname = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
|
||||
return localHostname;
|
||||
}
|
||||
}
|
||||
|
||||
private saveNewApp(instance: string, app: AppData): Promise<any> {
|
||||
const appInfo = new AppInfo();
|
||||
|
@ -1,17 +1,5 @@
|
||||
<div class="panel">
|
||||
<h3 class="panel__title">new message</h3>
|
||||
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<!-- <label>Please provide your account:</label> -->
|
||||
<input [(ngModel)]="title" type="text" class="form-control form-control-sm" name="title" autocomplete="off" placeholder="Title (optional)" />
|
||||
<!-- <textarea rows="4" cols="50"> -->
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm" style="min-width: 100%" rows="5" required placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
|
||||
|
||||
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy" [(ngModel)]="selectedPrivacy">
|
||||
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary">POST!</button>
|
||||
</form>
|
||||
|
||||
|
||||
<app-create-status (onClose)="closeColumn()"></app-create-status>
|
||||
</div>
|
@ -1,11 +1,5 @@
|
||||
import { Component, OnInit, Input, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Store } from '@ngxs/store';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { MastodonService, VisibilityEnum } from '../../../services/mastodon.service';
|
||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { NavigationService } from '../../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
@ -14,72 +8,13 @@ import { NavigationService } from '../../../services/navigation.service';
|
||||
styleUrls: ['./add-new-status.component.scss']
|
||||
})
|
||||
export class AddNewStatusComponent implements OnInit {
|
||||
@Input() title: string;
|
||||
@Input() status: string;
|
||||
@ViewChild('reply') replyElement: ElementRef;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly navigationService: NavigationService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
private readonly navigationService: NavigationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.focus();
|
||||
}, 0);
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
const accounts = this.getRegisteredAccounts();
|
||||
const selectedAccounts = accounts.filter(x => x.isSelected);
|
||||
|
||||
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
|
||||
switch (this.selectedPrivacy) {
|
||||
case 'Public':
|
||||
visibility = VisibilityEnum.Public;
|
||||
break;
|
||||
case 'Unlisted':
|
||||
visibility = VisibilityEnum.Unlisted;
|
||||
break;
|
||||
case 'Follows-only':
|
||||
visibility = VisibilityEnum.Private;
|
||||
break;
|
||||
case 'DM':
|
||||
visibility = VisibilityEnum.Direct;
|
||||
break;
|
||||
}
|
||||
|
||||
let spoiler = this.title;
|
||||
if(spoiler === '') {
|
||||
spoiler = null;
|
||||
}
|
||||
|
||||
for (const acc of selectedAccounts) {
|
||||
this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler)
|
||||
.then((res: Status) => {
|
||||
this.title = '';
|
||||
this.status = '';
|
||||
this.navigationService.closePanel();
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getRegisteredAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts;
|
||||
}
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
this.onSubmit();
|
||||
return false;
|
||||
closeColumn() {
|
||||
this.navigationService.closePanel();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@
|
||||
[browseThreadData]="overlayThreadToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="floating-column__header">
|
||||
<a class="close-button" href (click)="closePanel()" title="close">x</a>
|
||||
<a class="close-button" href (click)="closePanel()" title="close">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-manage-account *ngIf="openPanel === 'manageAccount'" [account]="userAccountUsed"></app-manage-account>
|
||||
|
@ -29,26 +29,32 @@
|
||||
}
|
||||
|
||||
.close-button {
|
||||
// display: inline-block;
|
||||
background-color: $color-primary;
|
||||
color: darken(white, 30);
|
||||
border-radius: 999px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
padding: 1px;
|
||||
|
||||
z-index: 9999;
|
||||
display: block;
|
||||
float: right;
|
||||
margin: 10px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
margin: 10px 16px 0 0;
|
||||
|
||||
transition: all .2s;
|
||||
// display: inline-block;
|
||||
// background-color: $color-primary;
|
||||
// color: darken(white, 30);
|
||||
// border-radius: 999px;
|
||||
// width: 26px;
|
||||
// height: 26px;
|
||||
// text-align: center;
|
||||
// text-decoration: none;
|
||||
// padding: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-primary, 20);
|
||||
color: white;
|
||||
// transform: scale(1.2);
|
||||
}
|
||||
// z-index: 9999;
|
||||
// float: right;
|
||||
// margin: 10px;
|
||||
|
||||
// transition: all .2s;
|
||||
|
||||
// &:hover {
|
||||
// background-color: lighten($color-primary, 20);
|
||||
// color: white;
|
||||
// // transform: scale(1.2);
|
||||
// }
|
||||
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { NavigationService, LeftPanelType } from '../../services/navigation.service';
|
||||
import { AccountWrapper } from '../../models/account.models';
|
||||
import { OpenThreadEvent } from '../../services/tools.service';
|
||||
@ -9,6 +11,7 @@ import { OpenThreadEvent } from '../../services/tools.service';
|
||||
styleUrls: ['./floating-column.component.scss']
|
||||
})
|
||||
export class FloatingColumnComponent implements OnInit {
|
||||
faTimes = faTimes;
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: string;
|
||||
overlayHashtagToBrowse: string;
|
||||
|
@ -5,8 +5,8 @@ import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { Results, Account } from '../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../stream/stream.component';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
@ -47,11 +47,11 @@ export class SearchComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
browseThread(openThreadEvent: OpenThreadEvent): boolean{
|
||||
if(openThreadEvent){
|
||||
browseThread(openThreadEvent: OpenThreadEvent): boolean {
|
||||
if (openThreadEvent) {
|
||||
this.browseThreadEvent.next(openThreadEvent);
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
browseAccount(accountName: string): boolean {
|
||||
@ -68,26 +68,25 @@ export class SearchComponent implements OnInit {
|
||||
this.hashtags.length = 0;
|
||||
this.isLoading = true;
|
||||
|
||||
const enabledAccounts = this.toolsService.getSelectedAccounts();
|
||||
//First candid implementation
|
||||
if (enabledAccounts.length > 0) {
|
||||
this.lastAccountUsed = enabledAccounts[0];
|
||||
this.mastodonService.search(this.lastAccountUsed, data, true)
|
||||
.then((results: Results) => {
|
||||
if (results) {
|
||||
this.accounts = results.accounts.slice(0, 5);
|
||||
this.hashtags = results.hashtags;
|
||||
this.lastAccountUsed = this.toolsService.getSelectedAccounts()[0];
|
||||
|
||||
for (let status of results.statuses) {
|
||||
const statusWrapper = new StatusWrapper(status, this.lastAccountUsed);
|
||||
this.statuses.push(statusWrapper);
|
||||
}
|
||||
this.mastodonService.search(this.lastAccountUsed, data, true)
|
||||
.then((results: Results) => {
|
||||
if (results) {
|
||||
this.accounts = results.accounts.slice(0, 5);
|
||||
this.hashtags = results.hashtags;
|
||||
|
||||
for (let status of results.statuses) {
|
||||
const statusWrapper = new StatusWrapper(status, this.lastAccountUsed);
|
||||
this.statuses.push(statusWrapper);
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
})
|
||||
.then(() => { this.isLoading = false; });
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
})
|
||||
.then(() => { this.isLoading = false; });
|
||||
}
|
||||
|
||||
private
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.notification-hub {
|
||||
position: fixed;
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
z-index: 9999999;
|
||||
margin: 0 0 10px 0;
|
||||
@ -10,6 +10,9 @@
|
||||
padding: 5px 10px;
|
||||
border-radius: 2px;
|
||||
margin: 0 0 5px 15px;
|
||||
max-width: 305px;
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
cursor: pointer;
|
||||
|
||||
&--error{
|
||||
|
@ -17,7 +17,7 @@ export class NotificationHubComponent implements OnInit {
|
||||
|
||||
setTimeout(() => {
|
||||
this.notifications = this.notifications.filter(x => x.id !== notification.id);
|
||||
}, 2000);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
//this.autoSubmit();
|
||||
|
@ -5,12 +5,12 @@ import { Observable, Subscription } from 'rxjs';
|
||||
import { faWindowClose, faReply, faRetweet, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
import { StatusWrapper } from '../../stream.component';
|
||||
import { MastodonService } from '../../../../services/mastodon.service';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { StatusWrapper } from '../../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-action-bar',
|
||||
@ -186,8 +186,8 @@ export class ActionBarComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
private getSelectedAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts;
|
||||
}
|
||||
// private getSelectedAccounts(): AccountInfo[] {
|
||||
// var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
// return regAccounts;
|
||||
// }
|
||||
}
|
||||
|
@ -21,4 +21,7 @@
|
||||
//font-size: .9em;
|
||||
// font-size: 14px;
|
||||
}
|
||||
& p:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
@ -60,6 +60,13 @@ describe('DatabindedTextComponent', () => {
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
||||
it('should parse https://www. link', () => {
|
||||
const url = 'bbc.com/news/magazine-34901704';
|
||||
const sample = `<p>The rise of"<br><a href="https:www//${url}" rel="nofollow noopener" target="_blank"><span class="invisible">https://www.</span><span class="">${url}</span><span class="invisible"></span></a></p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwbbccomnewsmagazine34901704" title="open link">bbc.com/news/magazine-34901704</a></p>');
|
||||
});
|
||||
|
||||
it('should parse link - dual section', () => {
|
||||
const sample = `<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">peertube.fr/videos/watch/69bb6</span><span class="invisible">e90-ec0f-49a3-9e28-41792f4a7c5f</span></a></p>`;
|
||||
|
||||
@ -89,7 +96,7 @@ describe('DatabindedTextComponent', () => {
|
||||
expect(component.processedText).toContain('bla2');
|
||||
expect(component.processedText).toContain('bla3');
|
||||
expect(component.processedText).toContain('bla4');
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse link - GNU social in Mastodon', () => {
|
||||
const sample = `bla1 <a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" rel="nofollow noopener" class="" target="_blank">https://social.bitcast.info/url/819438</a>`;
|
||||
|
@ -22,6 +22,9 @@ export class DatabindedTextComponent implements OnInit {
|
||||
|
||||
@Input('text')
|
||||
set text(value: string) {
|
||||
// console.warn('text');
|
||||
// console.warn(value);
|
||||
|
||||
this.processedText = '';
|
||||
let linksSections = value.split('<a ');
|
||||
|
||||
@ -111,7 +114,7 @@ export class DatabindedTextComponent implements OnInit {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
|
||||
} catch (err) {
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="invisible">https://</span><span class="">')[1].split('</span>')[0];
|
||||
extractedName = extractedLinkAndNext[0].split(`<span class="">`)[1].split('</span>')[0];
|
||||
}
|
||||
catch (err) {
|
||||
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
|
||||
@ -169,6 +172,16 @@ export class DatabindedTextComponent implements OnInit {
|
||||
window.open(link, '_blank');
|
||||
return false;
|
||||
});
|
||||
|
||||
this.renderer.listen(el, 'mouseup', (event) => {
|
||||
if(event.which === 2){
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
window.open(link, '_blank');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
<form (ngSubmit)="onSubmit()">
|
||||
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm" rows="5" required placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"></textarea>
|
||||
|
||||
<select class="form-control form-control-sm form-control--privacy" id="privacy" name="privacy" [(ngModel)]="selectedPrivacy">
|
||||
<option *ngFor="let p of privacyList" [ngValue]="p">{{p}}</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-custom-primary">REPLY!</button>
|
||||
</form>
|
@ -1,36 +0,0 @@
|
||||
@import "variables";
|
||||
@import "panel";
|
||||
@import "buttons";
|
||||
$btn-send-status-width: 60px;
|
||||
.form-control {
|
||||
margin: 0 0 5px 5px;
|
||||
width: calc(100% - 10px);
|
||||
background-color: $column-color;
|
||||
border-color: $status-secondary-color;
|
||||
color: #fff;
|
||||
font-size: $default-font-size;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&--privacy {
|
||||
display: inline-block;
|
||||
width: calc(100% - 15px - #{$btn-send-status-width});
|
||||
}
|
||||
}
|
||||
|
||||
.btn-custom-primary {
|
||||
display: inline-block;
|
||||
width: $btn-send-status-width;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 5px; // background-color: orange;
|
||||
// border-color: orange;
|
||||
// color: black;
|
||||
font-weight: 500; // &:hover {
|
||||
// }
|
||||
// &:focus {
|
||||
// border-color: darkblue;
|
||||
// }
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
|
||||
// import { Store } from '@ngxs/store';
|
||||
import { MastodonService, VisibilityEnum } from '../../../../services/mastodon.service';
|
||||
// import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { StatusWrapper } from '../../stream.component';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
import { NotificationService } from '../../../../services/notification.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reply-to-status',
|
||||
templateUrl: './reply-to-status.component.html',
|
||||
styleUrls: ['./reply-to-status.component.scss']
|
||||
})
|
||||
export class ReplyToStatusComponent implements OnInit {
|
||||
@Input() status: string = '';
|
||||
@Input() statusReplyingToWrapper: StatusWrapper;
|
||||
@Output() onClose = new EventEmitter();
|
||||
@ViewChild('reply') replyElement: ElementRef;
|
||||
|
||||
private statusReplyingTo: Status;
|
||||
|
||||
selectedPrivacy = 'Public';
|
||||
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
|
||||
constructor(
|
||||
// private readonly store: Store,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.statusReplyingToWrapper.status.reblog) {
|
||||
this.statusReplyingTo = this.statusReplyingToWrapper.status.reblog;
|
||||
} else {
|
||||
this.statusReplyingTo = this.statusReplyingToWrapper.status;
|
||||
}
|
||||
|
||||
this.status += `@${this.statusReplyingTo.account.acct} `;
|
||||
for (const mention of this.statusReplyingTo.mentions) {
|
||||
this.status += `@${mention.acct} `;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.replyElement.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
|
||||
switch (this.selectedPrivacy) {
|
||||
case 'Public':
|
||||
visibility = VisibilityEnum.Public;
|
||||
break;
|
||||
case 'Unlisted':
|
||||
visibility = VisibilityEnum.Unlisted;
|
||||
break;
|
||||
case 'Follows-only':
|
||||
visibility = VisibilityEnum.Private;
|
||||
break;
|
||||
case 'DM':
|
||||
visibility = VisibilityEnum.Direct;
|
||||
break;
|
||||
}
|
||||
|
||||
let spoiler = this.statusReplyingTo.spoiler_text;
|
||||
|
||||
const selectedAccounts = this.toolsService.getSelectedAccounts();
|
||||
for (const acc of selectedAccounts) {
|
||||
|
||||
const usableStatus = this.toolsService.getStatusUsableByAccount(acc, this.statusReplyingToWrapper);
|
||||
usableStatus
|
||||
.then((status: Status) => {
|
||||
return this.mastodonService.postNewStatus(acc, this.status, visibility, spoiler, status.id);
|
||||
})
|
||||
.then((res: Status) => {
|
||||
this.status = '';
|
||||
this.onClose.emit();
|
||||
})
|
||||
.catch((err: HttpErrorResponse) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// private getRegisteredAccounts(): AccountInfo[] {
|
||||
// var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
// return regAccounts;
|
||||
// }
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
this.onSubmit();
|
||||
return false;
|
||||
}
|
||||
}
|
@ -20,38 +20,35 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="status__labels">
|
||||
<div class="status__labels--label status__labels--bot" title="bot"
|
||||
*ngIf="status.account.bot">
|
||||
<div class="status__labels--label status__labels--bot" title="bot" *ngIf="status.account.bot">
|
||||
bot
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--xpost" title="cross-poster"
|
||||
*ngIf="isCrossPoster">
|
||||
<div class="status__labels--label status__labels--xpost" title="this status was cross-posted" *ngIf="isCrossPoster">
|
||||
x-post
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--thread" title="thread"
|
||||
*ngIf="isThread">
|
||||
thread
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--thread" title="thread" *ngIf="isThread">
|
||||
thread
|
||||
</div>
|
||||
<div class="status__labels--label status__labels--discuss" title="this status has a discution" *ngIf="hasReply">
|
||||
replies
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
|
||||
|
||||
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content" (click)="removeContentWarning()">
|
||||
<a href class="status__content-warning" *ngIf="isContentWarned" title="show content"
|
||||
(click)="removeContentWarning()">
|
||||
<span class="status__content-warning--title">sensitive content</span>
|
||||
{{ contentWarningText }}
|
||||
</a>
|
||||
<app-databinded-text class="status__content" *ngIf="!isContentWarned"
|
||||
[text]="displayedStatus.content"
|
||||
(accountSelected)="accountSelected($event)"
|
||||
(hashtagSelected)="hashtagSelected($event)"
|
||||
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="displayedStatus.content"
|
||||
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
|
||||
(textSelected)="textSelected()"></app-databinded-text>
|
||||
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments" [attachments]="displayedStatus.media_attachments">
|
||||
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
|
||||
[attachments]="displayedStatus.media_attachments">
|
||||
</app-attachements>
|
||||
|
||||
<app-action-bar #appActionBar
|
||||
[statusWrapper]="statusWrapper"
|
||||
(cwIsActiveEvent)="changeCw($event)"
|
||||
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)"
|
||||
(replyEvent)="openReply()"></app-action-bar>
|
||||
|
||||
<app-reply-to-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()">
|
||||
</app-reply-to-status>
|
||||
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()"></app-create-status>
|
||||
</div>
|
@ -76,6 +76,9 @@
|
||||
background-color: rgb(0, 136, 61);
|
||||
background-color: rgb(0, 114, 51);
|
||||
}
|
||||
&--discuss {
|
||||
background-color: rgb(90, 0, 143);
|
||||
}
|
||||
}
|
||||
&__name {
|
||||
display: inline-block;
|
||||
@ -112,24 +115,25 @@
|
||||
display: block; // border: 1px solid greenyellow;
|
||||
margin: 0 10px 0 $avatar-column-space;
|
||||
padding: 3px 5px 3px 5px;
|
||||
background-color: $content-warning-background-color;
|
||||
color: $content-warning-font-color;
|
||||
border-radius: 3px;
|
||||
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
|
||||
border: 3px solid $status-secondary-color;
|
||||
color: whitesmoke;
|
||||
|
||||
&--title {
|
||||
// padding-top: 3px;
|
||||
color: whitesmoke;
|
||||
color: $content-warning-font-color;
|
||||
font-size: 11px;
|
||||
//font-weight: bold;
|
||||
// outline: 1px solid greenyellow;
|
||||
display: block;
|
||||
width: calc(100%);
|
||||
text-align: center;
|
||||
// position: absolute;
|
||||
// bottom: 5px;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
&__created-at {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ViewChild } from "@angular/core";
|
||||
import { Status, Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { StatusWrapper } from "../stream.component";
|
||||
import { OpenThreadEvent } from "../../../services/tools.service";
|
||||
import { ActionBarComponent } from "./action-bar/action-bar.component";
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: "app-status",
|
||||
@ -17,6 +17,7 @@ export class StatusComponent implements OnInit {
|
||||
isCrossPoster: boolean;
|
||||
isThread: boolean;
|
||||
isContentWarned: boolean;
|
||||
hasReply: boolean;
|
||||
contentWarningText: string;
|
||||
|
||||
@Output() browseAccountEvent = new EventEmitter<string>();
|
||||
@ -24,6 +25,8 @@ export class StatusComponent implements OnInit {
|
||||
@Output() browseThreadEvent = new EventEmitter<OpenThreadEvent>();
|
||||
@ViewChild('appActionBar') appActionBar: ActionBarComponent;
|
||||
|
||||
@Input() isThreadDisplay: boolean;
|
||||
|
||||
private _statusWrapper: StatusWrapper;
|
||||
status: Status;
|
||||
@Input('statusWrapper')
|
||||
@ -31,9 +34,6 @@ export class StatusComponent implements OnInit {
|
||||
this._statusWrapper = value;
|
||||
this.status = value.status;
|
||||
|
||||
this.checkLabels(this.status);
|
||||
this.checkContentWarning(this.status);
|
||||
|
||||
if (this.status.reblog) {
|
||||
this.reblog = true;
|
||||
this.displayedStatus = this.status.reblog;
|
||||
@ -41,6 +41,9 @@ export class StatusComponent implements OnInit {
|
||||
this.displayedStatus = this.status;
|
||||
}
|
||||
|
||||
this.checkLabels(this.displayedStatus);
|
||||
this.checkContentWarning(this.displayedStatus);
|
||||
|
||||
if (!this.displayedStatus.account.display_name) {
|
||||
this.displayedStatus.account.display_name = this.displayedStatus.account.username;
|
||||
}
|
||||
@ -71,25 +74,31 @@ export class StatusComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
changeCw(cwIsActive: boolean){
|
||||
changeCw(cwIsActive: boolean) {
|
||||
this.isContentWarned = cwIsActive;
|
||||
}
|
||||
|
||||
private checkLabels(status: Status) {
|
||||
//since API is limited with federated status...
|
||||
if (status.uri.includes('birdsite.link')) {
|
||||
this.isCrossPoster = true;
|
||||
}
|
||||
else if (status.application) {
|
||||
const usedApp = status.application.name.toLowerCase();
|
||||
if (usedApp && (usedApp.includes('moa') || usedApp.includes('birdsite') || usedApp.includes('twitter'))) {
|
||||
if (!status.account.bot) {
|
||||
if (status.uri.includes('birdsite.link')) {
|
||||
this.isCrossPoster = true;
|
||||
}
|
||||
else if (status.application) {
|
||||
const usedApp = status.application.name.toLowerCase();
|
||||
if (usedApp && (usedApp.includes('moa') || usedApp.includes('birdsite') || usedApp.includes('twitter'))) {
|
||||
this.isCrossPoster = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.status.in_reply_to_account_id && this.status.in_reply_to_account_id === this.status.account.id) {
|
||||
if(this.isThreadDisplay) return;
|
||||
|
||||
if (status.in_reply_to_account_id && status.in_reply_to_account_id === status.account.id) {
|
||||
this.isThread = true;
|
||||
}
|
||||
|
||||
this.hasReply = status.replies_count > 0;
|
||||
}
|
||||
|
||||
openAccount(account: Account): boolean {
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
<!-- data-simplebar -->
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper"
|
||||
<app-status
|
||||
[statusWrapper]="statusWrapper"
|
||||
[isThreadDisplay]="isThread"
|
||||
(browseAccountEvent)="browseAccount($event)"
|
||||
(browseHashtagEvent)="browseHashtag($event)"
|
||||
(browseThreadEvent)="browseThread($event)"></app-status>
|
||||
|
@ -8,9 +8,9 @@ import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StreamingService, EventEnum, StreamingWrapper, StatusUpdate } from '../../../services/streaming.service';
|
||||
import { Status } from '../../../services/models/mastodon.interfaces';
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { StatusWrapper } from '../stream.component';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-statuses',
|
||||
@ -19,6 +19,7 @@ import { OpenThreadEvent, ToolsService } from '../../../services/tools.service';
|
||||
})
|
||||
export class StreamStatusesComponent implements OnInit, OnDestroy {
|
||||
isLoading = true;
|
||||
isThread = false;
|
||||
displayError: string;
|
||||
|
||||
private _streamElement: StreamElement;
|
||||
|
@ -97,9 +97,3 @@ export class StreamComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusWrapper {
|
||||
constructor(
|
||||
public status: Status,
|
||||
public provider: AccountInfo
|
||||
) { }
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
import { StatusWrapper } from '../stream.component';
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
|
||||
import { Results, Context, Status } from '../../../services/models/mastodon.interfaces';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StreamStatusesComponent } from '../stream-statuses/stream-statuses.component';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-thread',
|
||||
@ -18,6 +17,7 @@ export class ThreadComponent implements OnInit {
|
||||
statuses: StatusWrapper[] = [];
|
||||
displayError: string;
|
||||
isLoading = true;
|
||||
isThread = true;
|
||||
|
||||
private lastThreadEvent: OpenThreadEvent;
|
||||
|
||||
|
@ -8,9 +8,9 @@ import { Store } from '@ngxs/store';
|
||||
import { Account, Status, Relationship } from "../../../services/models/mastodon.interfaces";
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { ToolsService, OpenThreadEvent } from '../../../services/tools.service';
|
||||
import { StatusWrapper } from '../stream.component';
|
||||
import { NotificationService } from '../../../services/notification.service';
|
||||
import { AccountInfo } from '../../../states/accounts.state';
|
||||
import { StatusWrapper } from '../../../models/common.model';
|
||||
|
||||
|
||||
|
||||
|
@ -1,8 +1,16 @@
|
||||
import { Attachment } from "../services/models/mastodon.interfaces";
|
||||
import { Attachment, Status } from "../services/models/mastodon.interfaces";
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
|
||||
export class OpenMediaEvent {
|
||||
constructor(
|
||||
public selectedIndex: number,
|
||||
public attachments: Attachment[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusWrapper {
|
||||
constructor(
|
||||
public status: Status,
|
||||
public provider: AccountInfo
|
||||
) { }
|
||||
}
|
12
src/app/services/instances-info.service.spec.ts
Normal file
12
src/app/services/instances-info.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InstancesInfoService } from './instances-info.service';
|
||||
|
||||
xdescribe('InstancesInfoService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: InstancesInfoService = TestBed.get(InstancesInfoService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
58
src/app/services/instances-info.service.ts
Normal file
58
src/app/services/instances-info.service.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { MastodonService, VisibilityEnum } from './mastodon.service';
|
||||
import { Instance, Account } from './models/mastodon.interfaces';
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InstancesInfoService {
|
||||
private defaultMaxChars = 500;
|
||||
private cachedMaxInstanceChar: { [id: string]: Promise<number>; } = {};
|
||||
private cachedDefaultPrivacy: { [id: string]: Promise<VisibilityEnum>; } = {};
|
||||
|
||||
constructor(private mastodonService: MastodonService) { }
|
||||
|
||||
getMaxStatusChars(instance: string): Promise<number> {
|
||||
if (!this.cachedMaxInstanceChar[instance]) {
|
||||
this.cachedMaxInstanceChar[instance] = this.mastodonService.getInstance(instance)
|
||||
.then((instance: Instance) => {
|
||||
if (instance.max_toot_chars) {
|
||||
return instance.max_toot_chars;
|
||||
} else {
|
||||
return this.defaultMaxChars;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return this.defaultMaxChars;
|
||||
});
|
||||
}
|
||||
return this.cachedMaxInstanceChar[instance];
|
||||
}
|
||||
|
||||
getDefaultPrivacy(account: AccountInfo): Promise<VisibilityEnum> {
|
||||
const instance = account.instance;
|
||||
if (!this.cachedDefaultPrivacy[instance]) {
|
||||
this.cachedDefaultPrivacy[instance] = this.mastodonService.retrieveAccountDetails(account)
|
||||
.then((accountDetails: Account) => {
|
||||
switch (accountDetails.source.privacy) {
|
||||
case 'public':
|
||||
return VisibilityEnum.Public;
|
||||
case 'unlisted':
|
||||
return VisibilityEnum.Unlisted;
|
||||
case 'private':
|
||||
return VisibilityEnum.Private;
|
||||
case 'direct':
|
||||
return VisibilityEnum.Direct;
|
||||
default:
|
||||
return VisibilityEnum.Public;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return VisibilityEnum.Public;
|
||||
});
|
||||
}
|
||||
return this.cachedDefaultPrivacy[instance];
|
||||
}
|
||||
}
|
@ -2,18 +2,21 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpHeaders, HttpClient } from '@angular/common/http';
|
||||
|
||||
import { ApiRoutes } from './models/api.settings';
|
||||
import { Account, Status, Results, Context, Relationship } from "./models/mastodon.interfaces";
|
||||
import { Account, Status, Results, Context, Relationship, Instance, Attachment } from "./models/mastodon.interfaces";
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
import { StreamTypeEnum } from '../states/streams.state';
|
||||
import { stat } from 'fs';
|
||||
import { forEach } from '@angular/router/src/utils/collection';
|
||||
|
||||
@Injectable()
|
||||
export class MastodonService {
|
||||
export class MastodonService {
|
||||
private apiRoutes = new ApiRoutes();
|
||||
|
||||
constructor(private readonly httpClient: HttpClient) { }
|
||||
|
||||
getInstance(instance: string): Promise<Instance> {
|
||||
const route = `https://${instance}${this.apiRoutes.getInstance}`;
|
||||
return this.httpClient.get<Instance>(route).toPromise();
|
||||
}
|
||||
|
||||
retrieveAccountDetails(account: AccountInfo): Promise<Account> {
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise();
|
||||
@ -22,7 +25,7 @@ export class MastodonService {
|
||||
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20, tag: string = null, list: string = null): Promise<Status[]> {
|
||||
const route = `https://${account.instance}${this.getTimelineRoute(type, max_id, since_id, limit, tag, list)}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
|
||||
return this.httpClient.get<Status[]>(route, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
private getTimelineRoute(type: StreamTypeEnum, max_id: string, since_id: string, limit: number, tag: string, list: string): string {
|
||||
@ -69,67 +72,61 @@ export class MastodonService {
|
||||
return origString.replace(regEx, "");
|
||||
};
|
||||
|
||||
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null): Promise<Status> {
|
||||
postNewStatus(account: AccountInfo, status: string, visibility: VisibilityEnum, spoiler: string = null, in_reply_to_id: string = null, mediaIds: string[]): Promise<Status> {
|
||||
const url = `https://${account.instance}${this.apiRoutes.postNewStatus}`;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('status', status);
|
||||
|
||||
// formData.append('media_ids', media_ids);
|
||||
// formData.append('language', '');
|
||||
const statusData = new StatusData();
|
||||
statusData.status = status;
|
||||
statusData.media_ids = mediaIds;
|
||||
|
||||
if (in_reply_to_id) {
|
||||
formData.append('in_reply_to_id', in_reply_to_id);
|
||||
statusData.in_reply_to_id = in_reply_to_id;
|
||||
}
|
||||
|
||||
if (spoiler) {
|
||||
formData.append('sensitive', 'true');
|
||||
formData.append('spoiler_text', spoiler);
|
||||
statusData.sensitive = true;
|
||||
statusData.spoiler_text = spoiler;
|
||||
}
|
||||
|
||||
switch (visibility) {
|
||||
case VisibilityEnum.Public:
|
||||
formData.append('visibility', 'public');
|
||||
statusData.visibility = 'public';
|
||||
break;
|
||||
case VisibilityEnum.Unlisted:
|
||||
formData.append('visibility', 'unlisted');
|
||||
statusData.visibility = 'unlisted';
|
||||
break;
|
||||
case VisibilityEnum.Private:
|
||||
formData.append('visibility', 'private');
|
||||
statusData.visibility = 'private';
|
||||
break;
|
||||
case VisibilityEnum.Direct:
|
||||
formData.append('visibility', 'direct');
|
||||
statusData.visibility = 'direct';
|
||||
break;
|
||||
default:
|
||||
formData.append('visibility', 'private');
|
||||
statusData.visibility = 'private';
|
||||
break;
|
||||
}
|
||||
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
|
||||
return this.httpClient.post<Status>(url, formData, { headers: headers }).toPromise();
|
||||
return this.httpClient.post<Status>(url, statusData, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
search(account: AccountInfo, query: string, resolve: boolean = false): Promise<Results>{
|
||||
if(query[0] === '#') query = query.substr(1);
|
||||
search(account: AccountInfo, query: string, resolve: boolean = false): Promise<Results> {
|
||||
if (query[0] === '#') query = query.substr(1);
|
||||
const route = `https://${account.instance}${this.apiRoutes.search}?q=${query}&resolve=${resolve}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Results>(route, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
getAccountStatuses(account: AccountInfo, targetAccountId: number, onlyMedia: boolean, onlyPinned: boolean, excludeReplies: boolean, maxId: string, sinceId: string, limit: number = 20): Promise<Status[]>{
|
||||
getAccountStatuses(account: AccountInfo, targetAccountId: number, onlyMedia: boolean, onlyPinned: boolean, excludeReplies: boolean, maxId: string, sinceId: string, limit: number = 20): Promise<Status[]> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.getAccountStatuses}`.replace('{0}', targetAccountId.toString());
|
||||
let params = `?only_media=${onlyMedia}&pinned=${onlyPinned}&exclude_replies=${excludeReplies}&limit=${limit}`;
|
||||
|
||||
if(maxId) params += `&max_id=${maxId}`;
|
||||
if(sinceId) params += `&since_id=${sinceId}`;
|
||||
if (maxId) params += `&max_id=${maxId}`;
|
||||
if (sinceId) params += `&since_id=${sinceId}`;
|
||||
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Status[]>(route+params, { headers: headers }).toPromise();
|
||||
return this.httpClient.get<Status[]>(route + params, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
getStatusContext(account: AccountInfo, targetStatusId: string): Promise<Context>{
|
||||
getStatusContext(account: AccountInfo, targetStatusId: string): Promise<Context> {
|
||||
const params = this.apiRoutes.getStatusContext.replace('{0}', targetStatusId);
|
||||
const route = `https://${account.instance}${params}`;
|
||||
|
||||
@ -137,7 +134,7 @@ export class MastodonService {
|
||||
return this.httpClient.get<Context>(route, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false): Promise<Account[]>{
|
||||
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false): Promise<Account[]> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Account[]>(route, { headers: headers }).toPromise()
|
||||
@ -149,7 +146,7 @@ export class MastodonService {
|
||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
unreblog(account: AccountInfo, status: Status): Promise<Status> {
|
||||
unreblog(account: AccountInfo, status: Status): Promise<Status> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.unreblogStatus}`.replace('{0}', status.id);
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||
@ -160,30 +157,33 @@ export class MastodonService {
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
|
||||
unfavorite(account: AccountInfo, status: Status): any {
|
||||
const route = `https://${account.instance}${this.apiRoutes.unfavouritingStatus}`.replace('{0}', status.id);
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.post<Status>(route, null, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
|
||||
|
||||
getRelationships(account: AccountInfo, accountsToRetrieve: Account[]): Promise<Relationship[]> {
|
||||
let params = "?";
|
||||
accountsToRetrieve.forEach(x => {
|
||||
if(params.includes('id')) params += '&';
|
||||
params += `id[]=${x.id}`;
|
||||
});
|
||||
let params = `?${this.formatArray(accountsToRetrieve.map(x => x.id.toString()), 'id')}`;
|
||||
|
||||
// accountsToRetrieve.forEach(x => {
|
||||
// if (params.includes('id')) params += '&';
|
||||
// params += `id[]=${x.id}`;
|
||||
// });
|
||||
|
||||
const route = `https://${account.instance}${this.apiRoutes.getAccountRelationships}${params}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Relationship[]>(route, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
follow(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
|
||||
follow(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
|
||||
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.follow}`.replace('{0}', account.id.toString());
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${currentlyUsedAccount.token.access_token}` });
|
||||
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
|
||||
}
|
||||
}
|
||||
|
||||
unfollow(currentlyUsedAccount: AccountInfo, account: Account): Promise<Relationship> {
|
||||
const route = `https://${currentlyUsedAccount.instance}${this.apiRoutes.unfollow}`.replace('{0}', account.id.toString());
|
||||
@ -191,7 +191,33 @@ export class MastodonService {
|
||||
return this.httpClient.post<Relationship>(route, null, { headers: headers }).toPromise();
|
||||
|
||||
}
|
||||
|
||||
|
||||
uploadMediaAttachment(account: AccountInfo, file: File, description: string): Promise<Attachment> {
|
||||
let input = new FormData();
|
||||
input.append('file', file);
|
||||
input.append('description', description);
|
||||
const route = `https://${account.instance}${this.apiRoutes.uploadMediaAttachment}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.post<Attachment>(route, input, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
//TODO: add focus support
|
||||
updateMediaAttachment(account: AccountInfo, mediaId: string, description: string): Promise<Attachment> {
|
||||
let input = new FormData();
|
||||
input.append('description', description);
|
||||
const route = `https://${account.instance}${this.apiRoutes.updateMediaAttachment.replace('{0}', mediaId)}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
private formatArray(data: string[], paramName: string): string {
|
||||
let result = '';
|
||||
data.forEach(x => {
|
||||
if (result.includes('paramName')) result += '&';
|
||||
result += `${paramName}[]=${x}`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export enum VisibilityEnum {
|
||||
@ -200,4 +226,13 @@ export enum VisibilityEnum {
|
||||
Unlisted = 2,
|
||||
Private = 3,
|
||||
Direct = 4
|
||||
}
|
||||
|
||||
class StatusData {
|
||||
status: string;
|
||||
media_ids: string[];
|
||||
in_reply_to_id: string;
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
visibility: string;
|
||||
}
|
12
src/app/services/media.service.spec.ts
Normal file
12
src/app/services/media.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MediaService } from './media.service';
|
||||
|
||||
xdescribe('MediaService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: MediaService = TestBed.get(MediaService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
108
src/app/services/media.service.ts
Normal file
108
src/app/services/media.service.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
import { Attachment } from './models/mastodon.interfaces';
|
||||
import { MastodonService } from './mastodon.service';
|
||||
import { NotificationService } from './notification.service';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MediaService {
|
||||
mediaSubject: BehaviorSubject<MediaWrapper[]> = new BehaviorSubject<MediaWrapper[]>([]);
|
||||
|
||||
constructor(
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
uploadMedia(account: AccountInfo, files: File[]) {
|
||||
for (let file of files) {
|
||||
this.postMedia(account, file);
|
||||
}
|
||||
}
|
||||
|
||||
private postMedia(account: AccountInfo, file: File) {
|
||||
const uniqueId = `${file.name}${file.size}${Math.random()}`;
|
||||
const wrapper = new MediaWrapper(uniqueId, file, null);
|
||||
|
||||
let medias = this.mediaSubject.value;
|
||||
medias.push(wrapper);
|
||||
this.mediaSubject.next(medias);
|
||||
|
||||
this.mastodonService.uploadMediaAttachment(account, file, null)
|
||||
.then((attachment: Attachment) => {
|
||||
let currentMedias = this.mediaSubject.value;
|
||||
let currentMedia = currentMedias.filter(x => x.id === uniqueId)[0];
|
||||
if (currentMedia) {
|
||||
currentMedia.attachment = attachment;
|
||||
this.mediaSubject.next(currentMedias);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.remove(wrapper);
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
|
||||
update(account: AccountInfo, media: MediaWrapper) {
|
||||
if (media.attachment.description === media.description) return;
|
||||
|
||||
this.mastodonService.updateMediaAttachment(account, media.attachment.id, media.description)
|
||||
.then((att: Attachment) => {
|
||||
let medias = this.mediaSubject.value;
|
||||
let updatedMedia = medias.filter(x => x.id === media.id)[0];
|
||||
updatedMedia.attachment.description = att.description;
|
||||
this.mediaSubject.next(medias);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
|
||||
remove(media: MediaWrapper) {
|
||||
let medias = this.mediaSubject.value;
|
||||
let filteredMedias = medias.filter(x => x.id !== media.id);
|
||||
this.mediaSubject.next(filteredMedias);
|
||||
}
|
||||
|
||||
clearMedia() {
|
||||
this.mediaSubject.next([]);
|
||||
}
|
||||
|
||||
migrateMedias(account: AccountInfo) {
|
||||
let medias = this.mediaSubject.value;
|
||||
medias.forEach(media => {
|
||||
media.isMigrating = true;
|
||||
});
|
||||
this.mediaSubject.next(medias);
|
||||
|
||||
for (let media of medias) {
|
||||
this.mastodonService.uploadMediaAttachment(account, media.file, media.description)
|
||||
.then((attachment: Attachment) => {
|
||||
let currentMedias = this.mediaSubject.value;
|
||||
let currentMedia = currentMedias.filter(x => x.id === media.id)[0];
|
||||
if (currentMedia) {
|
||||
currentMedia.attachment = attachment;
|
||||
currentMedia.isMigrating = false;
|
||||
this.mediaSubject.next(currentMedias);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.remove(media);
|
||||
this.notificationService.notifyHttpError(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaWrapper {
|
||||
constructor(
|
||||
public id: string,
|
||||
public file: File,
|
||||
public attachment: Attachment) { }
|
||||
|
||||
public description: string;
|
||||
public isMigrating: boolean;
|
||||
}
|
@ -23,6 +23,7 @@ export class ApiRoutes {
|
||||
followRemote = '/api/v1/follows';
|
||||
getInstance = '/api/v1/instance';
|
||||
uploadMediaAttachment = '/api/v1/media';
|
||||
updateMediaAttachment = '/api/v1/media/{0}';
|
||||
getMutes = '/api/v1/mutes';
|
||||
getNotifications = '/api/v1/notifications';
|
||||
getSingleNotifications = '/api/v1/notifications/{0}';
|
||||
|
@ -34,6 +34,14 @@ export interface Account {
|
||||
moved: boolean;
|
||||
fields: Field[];
|
||||
bot: boolean;
|
||||
source: AccountInfo;
|
||||
}
|
||||
|
||||
export interface AccountInfo {
|
||||
privacy: string;
|
||||
sensitive: boolean;
|
||||
note: string;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
export interface Emoji {
|
||||
@ -61,6 +69,8 @@ export interface Attachment {
|
||||
remote_url: string;
|
||||
preview_url: string;
|
||||
text_url: string;
|
||||
meta: any;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
@ -84,6 +94,10 @@ export interface Instance {
|
||||
title: string;
|
||||
description: string;
|
||||
email: string;
|
||||
version: string;
|
||||
urls: string[];
|
||||
contact_account: Account;
|
||||
max_toot_chars: number;
|
||||
}
|
||||
|
||||
export interface Mention {
|
||||
@ -131,7 +145,8 @@ export interface Status {
|
||||
reblog: Status;
|
||||
content: string;
|
||||
created_at: string;
|
||||
reblogs_count: string;
|
||||
reblogs_count: number;
|
||||
replies_count: number;
|
||||
favourites_count: string;
|
||||
reblogged: boolean;
|
||||
favourited: boolean;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Status } from "./models/mastodon.interfaces";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { Status } from "./models/mastodon.interfaces";
|
||||
import { ApiRoutes } from "./models/api.settings";
|
||||
import { StreamTypeEnum, StreamElement } from "../states/streams.state";
|
||||
import { MastodonService } from "./mastodon.service";
|
||||
import { AccountInfo } from "../states/accounts.state";
|
||||
import { stat } from "fs";
|
||||
|
||||
@Injectable()
|
||||
export class StreamingService {
|
||||
@ -18,8 +18,6 @@ export class StreamingService {
|
||||
getStreaming(accountInfo: AccountInfo, stream: StreamElement): StreamingWrapper {
|
||||
return new StreamingWrapper(this.mastodonService, accountInfo, stream, this.nbStatusPerIteration);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class StreamingWrapper {
|
||||
|
@ -4,8 +4,7 @@ import { Store } from '@ngxs/store';
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
import { MastodonService } from './mastodon.service';
|
||||
import { Account, Results, Status } from "./models/mastodon.interfaces";
|
||||
import { StatusWrapper } from '../components/stream/stream.component';
|
||||
|
||||
import { StatusWrapper } from '../models/common.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -52,7 +52,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body ondragstart="return false;" ondrop="return false;">
|
||||
<app-root>
|
||||
<div class="lds-ripple">
|
||||
<div></div>
|
||||
|
@ -1,49 +1,4 @@
|
||||
/**
|
||||
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||
* You can add your own extra polyfills to this file.
|
||||
*
|
||||
* This file is divided into 2 sections:
|
||||
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||
* file.
|
||||
*
|
||||
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||
*
|
||||
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||
*/
|
||||
|
||||
/***************************************************************************************************
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||
// import 'core-js/es6/symbol';
|
||||
// import 'core-js/es6/object';
|
||||
// import 'core-js/es6/function';
|
||||
// import 'core-js/es6/parse-int';
|
||||
// import 'core-js/es6/parse-float';
|
||||
// import 'core-js/es6/number';
|
||||
// import 'core-js/es6/math';
|
||||
// import 'core-js/es6/string';
|
||||
// import 'core-js/es6/date';
|
||||
// import 'core-js/es6/array';
|
||||
// import 'core-js/es6/regexp';
|
||||
// import 'core-js/es6/map';
|
||||
// import 'core-js/es6/weak-map';
|
||||
// import 'core-js/es6/set';
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
||||
import 'core-js/es7/reflect';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -9,6 +9,6 @@
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
margin: 6px 0 12px 0;
|
||||
margin: 4px 0 12px 5px;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
@import './variables';
|
||||
@import './mixins';
|
||||
|
||||
@import "bootstrap";
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
|
||||
*,
|
||||
*::after,
|
||||
@ -26,6 +26,7 @@ html, body {
|
||||
|
||||
color: $font-color-primary;
|
||||
background-color: $color-primary;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// .invisible {
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"outDir": "./dist/out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user