Merge pull request #142 from NicolasConstant/topic_emoji-picker

Topic emoji picker
This commit is contained in:
Nicolas Constant 2019-07-29 19:34:17 -04:00 committed by GitHub
commit 2dea5544eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 403 additions and 100 deletions

View File

@ -21,7 +21,8 @@
"src/favicon.ico"
],
"styles": [
"src/sass/styles.scss"
"src/sass/styles.scss",
"node_modules/@ctrl/ngx-emoji-mart/picker.css"
],
"stylePreprocessorOptions": {
"includePaths": [

10
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.11.0",
"version": "0.12.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -711,6 +711,14 @@
}
}
},
"@ctrl/ngx-emoji-mart": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@ctrl/ngx-emoji-mart/-/ngx-emoji-mart-0.17.0.tgz",
"integrity": "sha512-gdHM/OPTbqWMIlFPAbjgAPo5BGsjkehILCInw5OttuT25HMZXJFjWVpi6vGixNVrAs8kz6sTYM/wbldS5GP9yQ==",
"requires": {
"tslib": "^1.9.0"
}
},
"@fortawesome/angular-fontawesome": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.3.0.tgz",

View File

@ -37,6 +37,7 @@
"@angular/platform-browser": "^7.2.7",
"@angular/platform-browser-dynamic": "^7.2.7",
"@angular/router": "^7.2.7",
"@ctrl/ngx-emoji-mart": "^0.17.0",
"@fortawesome/angular-fontawesome": "^0.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.13",
"@fortawesome/free-brands-svg-icons": "^5.7.0",

View File

@ -9,9 +9,11 @@ import { RouterModule, Routes } from "@angular/router";
import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
import { OverlayModule } from '@angular/cdk/overlay';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { ContextMenuModule } from 'ngx-contextmenu';
import { PickerModule } from '@ctrl/ngx-emoji-mart';
import { AppComponent } from "./app.component";
import { LeftSideBarComponent } from "./components/left-side-bar/left-side-bar.component";
@ -64,78 +66,86 @@ import { ListAccountComponent } from './components/floating-column/manage-accoun
import { PollComponent } from './components/stream/status/poll/poll.component';
import { TimeLeftPipe } from './pipes/time-left.pipe';
import { AutosuggestComponent } from './components/create-status/autosuggest/autosuggest.component';
import { EmojiPickerComponent } from './components/create-status/emoji-picker/emoji-picker.component';
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: StreamsMainDisplayComponent },
{ path: "register", component: RegisterNewAccountComponent},
{ path: "**", redirectTo: "home" }
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: StreamsMainDisplayComponent },
{ path: "register", component: RegisterNewAccountComponent },
{ path: "**", redirectTo: "home" }
];
@NgModule({
declarations: [
AppComponent,
LeftSideBarComponent,
StreamsMainDisplayComponent,
StreamComponent,
StreamsSelectionFooterComponent,
StatusComponent,
RegisterNewAccountComponent,
AccountIconComponent,
FloatingColumnComponent,
ManageAccountComponent,
AddNewStatusComponent,
AttachementsComponent,
SettingsComponent,
AddNewAccountComponent,
SearchComponent,
ActionBarComponent,
WaitingAnimationComponent,
UserProfileComponent,
ThreadComponent,
HashtagComponent,
StreamOverlayComponent,
DatabindedTextComponent,
TimeAgoPipe,
StreamStatusesComponent,
StreamEditionComponent,
TutorialComponent,
NotificationHubComponent,
MediaViewerComponent,
CreateStatusComponent,
MediaComponent,
MyAccountComponent,
FavoritesComponent,
DirectMessagesComponent,
MentionsComponent,
NotificationsComponent,
AccountEmojiPipe,
CardComponent,
ListEditorComponent,
ListAccountComponent,
PollComponent,
TimeLeftPipe,
AutosuggestComponent
],
imports: [
FontAwesomeModule,
BrowserModule,
HttpModule,
HttpClientModule,
FormsModule,
RouterModule.forRoot(routes),
declarations: [
AppComponent,
LeftSideBarComponent,
StreamsMainDisplayComponent,
StreamComponent,
StreamsSelectionFooterComponent,
StatusComponent,
RegisterNewAccountComponent,
AccountIconComponent,
FloatingColumnComponent,
ManageAccountComponent,
AddNewStatusComponent,
AttachementsComponent,
SettingsComponent,
AddNewAccountComponent,
SearchComponent,
ActionBarComponent,
WaitingAnimationComponent,
UserProfileComponent,
ThreadComponent,
HashtagComponent,
StreamOverlayComponent,
DatabindedTextComponent,
TimeAgoPipe,
StreamStatusesComponent,
StreamEditionComponent,
TutorialComponent,
NotificationHubComponent,
MediaViewerComponent,
CreateStatusComponent,
MediaComponent,
MyAccountComponent,
FavoritesComponent,
DirectMessagesComponent,
MentionsComponent,
NotificationsComponent,
AccountEmojiPipe,
CardComponent,
ListEditorComponent,
ListAccountComponent,
PollComponent,
TimeLeftPipe,
AutosuggestComponent,
EmojiPickerComponent
],
entryComponents: [
EmojiPickerComponent
],
imports: [
FontAwesomeModule,
BrowserModule,
HttpModule,
HttpClientModule,
FormsModule,
PickerModule,
OverlayModule,
RouterModule.forRoot(routes),
NgxsModule.forRoot([
RegisteredAppsState,
AccountsState,
StreamsState,
SettingsState
]),
NgxsStoragePluginModule.forRoot(),
ContextMenuModule.forRoot()
],
providers: [AuthService, NavigationService, NotificationService, MastodonService, StreamingService],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
NgxsModule.forRoot([
RegisteredAppsState,
AccountsState,
StreamsState,
SettingsState
]),
NgxsStoragePluginModule.forRoot(),
ContextMenuModule.forRoot()
],
providers: [AuthService, NavigationService, NotificationService, MastodonService, StreamingService],
bootstrap: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }

View File

@ -2,12 +2,18 @@
<input [(ngModel)]="title" type="text" class="form-control form-control-sm status-editor__title" 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-editor__content" rows="5" required
title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()"></textarea>
<a class="status-editor__emoji" title="Insert Emoji"
#emojiButton href (click)="openEmojiPicker($event)">
<img class="status-editor__emoji--image" src="/assets/emoji/72x72/1f636.png">
</a>
<div class="status-editor__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to the
<textarea #reply [(ngModel)]="status" name="status" class="form-control form-control-sm status-editor__content"
rows="5" required title="content" placeholder="What's in your mind?" (keydown.control.enter)="onCtrlEnter()"
(keydown)="handleKeyDown($event)" (blur)="statusTextEditorLostFocus()">
</textarea>
<div class="status-editor__mention-error" *ngIf="mentionTooFarAwayError">Error: mentions must be placed closer to
the
start in order to use multiposting.</div>
<app-autosuggest class="status-editor__autosuggest" *ngIf="autosuggestData" [pattern]="autosuggestData"
@ -43,7 +49,6 @@
</a>
</div>
<context-menu #contextMenu>
<ng-template contextMenuItem (execute)="changePrivacy('Public')">
<fa-icon [icon]="faGlobeAmericas" class="context-menu-icon"></fa-icon> Public

View File

@ -6,6 +6,8 @@
$btn-send-status-width: 60px;
$counter-width: 90px;
// @import "~@ctrl/ngx-emoji-mart/picker";
.form-control {
margin: 0 0 5px 5px;
width: calc(100% - 10px);
@ -38,6 +40,35 @@ $counter-width: 90px;
margin-bottom: 0;
}
&__emoji {
position: absolute;
top: 37px;
right: 10px;
&--image {
transition: all .2s;
width: 24px;
height: 24px;
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
filter: gray;
opacity: .7;
&:hover {
filter: none;
-webkit-filter: grayscale(0%);
-moz-filter: grayscale(0%);
-ms-filter: grayscale(0%);
-o-filter: grayscale(0%);
opacity: 1;
}
}
}
&__content {
border-width: 0;
background-color: $status-editor-background;
@ -57,6 +88,7 @@ $counter-width: 90px;
height: 110px;
padding-bottom: 10px;
padding-right: 30px;
//border-bottom: 1px solid black;
&::-webkit-resizer {
@ -138,8 +170,8 @@ $counter-width: 90px;
width: $btn-send-status-width;
position: relative;
top: -1px;
left: 5px;
font-weight: 500;
left: 5px;
font-weight: 500;
}
.context-menu-icon {
@ -147,4 +179,21 @@ $counter-width: 90px;
left: -3px;
font-size: 12px;
color: #1f1f1f;
}
}
.emojipicker {
font-size: $default-font-size !important;
}
@import '~@angular/cdk/overlay-prebuilt.css';
// ::ng-deep .cdk-overlay-backdrop {
// // width: 100%;
// // height: 100%;
// border: 3px solid greenyellow;
// background-color: black;
// min-height: 20px;
// }

View File

@ -50,6 +50,57 @@ describe('CreateStatusComponent', () => {
expect(component).toBeTruthy();
});
it('should not count emoji as multiple chars', () => {
const status = '😃 😍 👌 👇 😱 😶 status with 😱 😶 emojis 😏 👍 ';
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(461);
});
it('should not count emoji in CW as multiple chars', () => {
const status = 'test';
(<any>component).title = '🙂 test';
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(490);
});
it('should not count domain chars in username', () => {
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqsdqsd';
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(466);
});
it('should not count https link more than the minimum', () => {
const status = "https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(477);
});
it('should not count http link more than the minimum', () => {
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(477);
});
it('should not count links more than the minimum', () => {
const status = "http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/";
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(429);
});
it('should count correctly complex status', () => {
const status = 'dsqdqs @NicolasConstant@mastodon.partipirate.org dsqdqs👇😱 😶 status https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ #Pleroma with 😱 😶 emojis 😏 👍 #Mastodon @ddqsdqs @dsqdsq@dqsdsqqdsq';
(<any>component).title = '🙂 test';
(<any>component).maxCharLength = 500;
(<any>component).countStatusChar(status);
expect((<any>component).charCountLeft).toBe(373);
});
it('should not parse small status', () => {
const status = 'this is a cool status';
(<any>component).maxCharLength = 500;
@ -134,4 +185,13 @@ describe('CreateStatusComponent', () => {
expect(result[1]).toContain('@Lorem@ipsum.com ');
});
it('should parse long link properly for multiposting', () => {
const status = 'dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd qsd sqd qsd qsd dsqd qsd qsd sqd qsd sqd dsq http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/';
(<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[1]).toBe('http://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/');
});
});

View File

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, ElementRef, ViewChild, ViewContainerRef, ComponentRef, HostListener } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Store } from '@ngxs/store';
import { Subscription, Observable } from 'rxjs';
@ -16,6 +16,9 @@ import { AccountInfo } from '../../states/accounts.state';
import { InstancesInfoService } from '../../services/instances-info.service';
import { MediaService } from '../../services/media.service';
import { AutosuggestSelection, AutosuggestUserActionEnum } from './autosuggest/autosuggest.component';
import { Overlay, OverlayConfig, FullscreenOverlayContainer, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
import { EmojiPickerComponent } from './emoji-picker/emoji-picker.component';
@Component({
selector: 'app-create-status',
@ -44,18 +47,13 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private _status: string = '';
@Input('status')
set status(value: string) {
if (value) {
this.countStatusChar(value);
this.detectAutosuggestion(value);
this._status = value;
this.countStatusChar(value);
this.detectAutosuggestion(value);
this._status = value;
setTimeout(() => {
this.autoGrow();
}, 0);
} else {
this.autosuggestData = null;
}
setTimeout(() => {
this.autoGrow();
}, 0);
}
get status(): string {
return this._status;
@ -155,7 +153,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private readonly toolsService: ToolsService,
private readonly mastodonService: MastodonService,
private readonly instancesInfoService: InstancesInfoService,
private readonly mediaService: MediaService) {
private readonly mediaService: MediaService,
private readonly overlay: Overlay,
public viewContainerRef: ViewContainerRef) {
this.accounts$ = this.store.select(state => state.registeredaccounts.accounts);
}
@ -186,6 +186,8 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
this.statusLoaded = true;
this.focus();
this.innerHeight = window.innerHeight;
}
ngOnDestroy() {
@ -353,8 +355,9 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
const currentStatus = parseStatus[parseStatus.length - 1];
const statusExtraChars = this.getMentionExtraChars(status);
const linksExtraChars = this.getLinksExtraChars(status);
const statusLength = currentStatus.length - statusExtraChars;
const statusLength = [...currentStatus].length - statusExtraChars - linksExtraChars;
this.charCountLeft = this.maxCharLength - statusLength - this.getCwLength();
this.postCounts = parseStatus.length;
}
@ -362,7 +365,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
private getCwLength(): number {
let cwLength = 0;
if (this.title) {
cwLength = this.title.length;
cwLength = [...this.title].length;
}
return cwLength;
}
@ -504,6 +507,18 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
return results;
}
private getLinksExtraChars(status: string): number {
let mentionExtraChars = 0;
let links = status.split(' ').filter(x => x.startsWith('http://') || x.startsWith('https://'));
for (let link of links) {
if(link.length > 23){
mentionExtraChars += link.length - 23;
}
}
return mentionExtraChars;
}
private getMentionExtraChars(status: string): number {
let mentionExtraChars = 0;
let mentions = this.getMentionsFromStatus(status);
@ -591,7 +606,7 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
}
private autoGrow() {
let scrolling = (this.replyElement.nativeElement.scrollHeight);
let scrolling = (this.replyElement.nativeElement.scrollHeight);
if (scrolling > 110) {
this.replyElement.nativeElement.style.height = `0px`;
@ -609,4 +624,70 @@ export class CreateStatusComponent implements OnInit, OnDestroy {
$event.preventDefault();
$event.stopPropagation();
}
//https://stackblitz.com/edit/overlay-demo
@ViewChild('emojiButton') emojiButtonElement: ElementRef;
overlayRef: OverlayRef;
public innerHeight: number;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.innerHeight = window.innerHeight;
}
private emojiCloseSub: Subscription;
private emojiSelectedSub: Subscription;
private beforeEmojiCaretPosition: number;
openEmojiPicker(e: MouseEvent): boolean {
if (this.overlayRef) return false;
this.beforeEmojiCaretPosition = this.replyElement.nativeElement.selectionStart;
let topPosition = e.pageY;
if (this.innerHeight - e.pageY < 360) {
topPosition -= 360;
}
let config = new OverlayConfig();
config.positionStrategy = this.overlay.position()
.global()
.left(`${e.pageX - 283}px`)
.top(`${topPosition}px`);
config.hasBackdrop = true;
this.overlayRef = this.overlay.create(config);
// this.overlayRef.backdropClick().subscribe(() => {
// console.warn('wut?');
// this.overlayRef.dispose();
// });
let comp = new ComponentPortal(EmojiPickerComponent);
const compRef: ComponentRef<EmojiPickerComponent> = this.overlayRef.attach(comp);
this.emojiCloseSub = compRef.instance.closedEvent.subscribe(() => {
this.closeEmojiPanel();
});
this.emojiSelectedSub = compRef.instance.emojiSelectedEvent.subscribe((emoji) => {
if (emoji) {
this.status = [this.status.slice(0, this.beforeEmojiCaretPosition), emoji, ' ', this.status.slice(this.beforeEmojiCaretPosition)].join('').replace(' ', ' ');
this.beforeEmojiCaretPosition += emoji.length + 1;
this.closeEmojiPanel();
}
});
return false;
}
private closeEmojiPanel() {
if (this.emojiCloseSub) this.emojiCloseSub.unsubscribe();
if (this.emojiSelectedSub) this.emojiSelectedSub.unsubscribe();
if (this.overlayRef) this.overlayRef.dispose();
this.overlayRef = null;
this.focus(this.beforeEmojiCaretPosition);
}
closeEmoji(): boolean {
this.overlayRef.dispose();
return false;
}
}

View File

@ -0,0 +1,8 @@
<emoji-mart
[showPreview]="false"
[perLine]="7"
[isNative]="true"
[sheetSize]="16"
[emojiTooltip]="true"
(emojiSelect)="emojiSelected($event)"
class="emojipicker" title="Pick your emoji…" emoji="point_up"></emoji-mart>

View File

@ -0,0 +1,16 @@
::ng-deep .emoji-mart {
border-radius: 0 !important;
font-size: 10px !important;
}
::ng-deep .emoji-mart-emoji-native {
cursor: pointer !important;
}
::ng-deep .emoji-mart-emoji-native span {
position: relative;
top: 1px;
left: -1px;
font-size: 19px !important;
cursor: pointer !important;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EmojiPickerComponent } from './emoji-picker.component';
xdescribe('EmojiPickerComponent', () => {
let component: EmojiPickerComponent;
let fixture: ComponentFixture<EmojiPickerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EmojiPickerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EmojiPickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,35 @@
import { Component, OnInit, HostListener, ElementRef, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-emoji-picker',
templateUrl: './emoji-picker.component.html',
styleUrls: ['./emoji-picker.component.scss']
})
export class EmojiPickerComponent implements OnInit {
private init = false;
@Output('closed') public closedEvent = new EventEmitter();
@Output('emojiSelected') public emojiSelectedEvent = new EventEmitter<string>();
constructor(private eRef: ElementRef) { }
@HostListener('document:click', ['$event'])
clickout(event) {
if (!this.init) return;
if (!this.eRef.nativeElement.contains(event.target)) {
this.closedEvent.emit(null);
}
}
ngOnInit() {
setTimeout(() => {
this.init = true;
}, 0);
}
emojiSelected(select: any): boolean {
this.emojiSelectedEvent.next(select.emoji.native);
return false;
}
}

View File

@ -2,7 +2,7 @@
<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 && !m.audioType" class="media__loaded" title="{{m.file.name}}"
<div *ngIf="m.attachment !== null && m.attachment.type !== 'audio'" 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>
@ -14,15 +14,18 @@
<input class="media__loaded--description" [(ngModel)]="m.description" autocomplete="off"
placeholder="Describe for the visually impaired" />
</div>
<img *ngIf="!m.audioType" class="media__loaded--preview" src="{{m.attachment.preview_url}}" />
<img class="media__loaded--preview" src="{{m.attachment.preview_url}}" />
</div>
<div *ngIf="m.attachment !== null && m.audioType" class="audio">
<div *ngIf="m.attachment !== null && m.attachment.type === 'audio'" class="audio">
<button class="audio__button" title="remove" (click)="removeMedia(m)">
<fa-icon [icon]="faTimes"></fa-icon>
</button>
<audio *ngIf="m.audioType" controls class="audio__player">
<div *ngIf="m.isMigrating">
<app-waiting-animation class="waiting-icon"></app-waiting-animation>
</div>
<audio *ngIf="m.audioType && !m.isMigrating" controls class="audio__player">
<source src="{{ m.attachment.url }}" type="{{ m.audioType }}">
Your browser does not support the audio element.
</audio>

View File

@ -71,7 +71,7 @@ export interface Application {
export interface Attachment {
id: string;
type: 'image' | 'video' | 'gifv';
type: 'image' | 'video' | 'gifv' | 'audio';
url: string;
remote_url: string;
preview_url: string;

View File

@ -9,6 +9,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"downlevelIteration": true,
"typeRoots": [
"node_modules/@types"
],