progress bar of file down is modified
This commit is contained in:
parent
a33ee7c2ff
commit
568ff48268
|
@ -241,8 +241,9 @@ ipcMain.on(
|
|||
try {
|
||||
const buffer: Buffer = args[0];
|
||||
const fileName: string = args[1];
|
||||
const mimeType: string = args[2];
|
||||
let savePath: string = path.join(
|
||||
!!args[2] ? args[2] : DefaultFolder.downloads(),
|
||||
!!args[3] ? args[3] : DefaultFolder.downloads(),
|
||||
fileName
|
||||
);
|
||||
savePath = await FileUtil.uniqueFileName(savePath);
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -2789,6 +2789,15 @@
|
|||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
|
||||
"dev": true
|
||||
},
|
||||
"angular-split": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-split/-/angular-split-3.0.2.tgz",
|
||||
"integrity": "sha512-km59k1kEgVlplo2t4t5Ob43Vx16qVXWXsl5gbsdQtqrOW7341So4CFUmCjcZgfk1swu9RBaCdSQEqzNWOe/89w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"ansi-align": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz",
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
<ucap-file-viewer
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-file-viewer>
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
OnInit,
|
||||
OnDestroy,
|
||||
Inject,
|
||||
EventEmitter
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
@ -15,7 +15,7 @@ import { take, map, finalize, tap } from 'rxjs/operators';
|
|||
import { SnackBarService } from '@ucap-webmessenger/ui';
|
||||
import {
|
||||
FileDownloadItem,
|
||||
CommonApiService
|
||||
CommonApiService,
|
||||
} from '@ucap-webmessenger/api-common';
|
||||
|
||||
export interface FileViewerDialogData {
|
||||
|
@ -31,7 +31,7 @@ export interface FileViewerDialogResult {}
|
|||
@Component({
|
||||
selector: 'app-layout-common-file-viewer',
|
||||
templateUrl: './file-viewer.dialog.component.html',
|
||||
styleUrls: ['./file-viewer.dialog.component.scss']
|
||||
styleUrls: ['./file-viewer.dialog.component.scss'],
|
||||
})
|
||||
export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
||||
fileInfo: FileEventJson;
|
||||
|
@ -40,8 +40,6 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
|||
deviceType: DeviceType;
|
||||
token: string;
|
||||
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
fileDownloadUrl: string;
|
||||
|
||||
constructor(
|
||||
|
@ -66,7 +64,7 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
|||
userSeq: this.userSeq,
|
||||
deviceType: this.deviceType,
|
||||
token: this.token,
|
||||
attachmentsSeq: this.fileInfo.attachmentSeq
|
||||
attachmentsSeq: this.fileInfo.attachmentSeq,
|
||||
},
|
||||
this.downloadUrl
|
||||
);
|
||||
|
@ -76,8 +74,7 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
onDownload(): void {
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
onDownload(fileDownloadItem: FileDownloadItem): void {
|
||||
this.commonApiService
|
||||
.fileTalkDownload(
|
||||
{
|
||||
|
@ -85,37 +82,36 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
|||
deviceType: this.deviceType,
|
||||
token: this.token,
|
||||
attachmentsSeq: this.fileInfo.attachmentSeq,
|
||||
fileDownloadItem: this.fileDownloadItem
|
||||
fileDownloadItem,
|
||||
},
|
||||
this.downloadUrl
|
||||
)
|
||||
.pipe(
|
||||
take(1),
|
||||
map(async rawBlob => {
|
||||
const blob = rawBlob.slice(
|
||||
0,
|
||||
rawBlob.size,
|
||||
MimeUtil.getMimeFromExtension(this.fileInfo.fileExt)
|
||||
);
|
||||
const mimeType = MimeUtil.getMimeFromExtension(this.fileInfo.fileExt);
|
||||
const blob = rawBlob.slice(0, rawBlob.size, mimeType);
|
||||
|
||||
FileUtil.fromBlobToBuffer(blob)
|
||||
.then(buffer => {
|
||||
this.nativeService
|
||||
.saveFile(buffer, this.fileInfo.fileName)
|
||||
.pipe(take(1))
|
||||
.subscribe(result => {
|
||||
.saveFile(buffer, this.fileInfo.fileName, mimeType)
|
||||
.then(result => {
|
||||
if (!!result) {
|
||||
this.snackBarService.open(
|
||||
`파일이 경로[${result}]에 저장되었습니다.`,
|
||||
'',
|
||||
{
|
||||
duration: 3000,
|
||||
verticalPosition: 'bottom'
|
||||
verticalPosition: 'bottom',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.snackBarService.open('파일 저장에 실패하였습니다.');
|
||||
}
|
||||
})
|
||||
.catch(reason => {
|
||||
this.snackBarService.open('파일 저장에 실패하였습니다.');
|
||||
});
|
||||
})
|
||||
.catch(reason => {
|
||||
|
@ -123,7 +119,9 @@ export class FileViewerDialogComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}),
|
||||
finalize(() => {
|
||||
this.fileDownloadItem = undefined;
|
||||
setTimeout(() => {
|
||||
fileDownloadItem.downloadingProgress$ = undefined;
|
||||
}, 1000);
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
|
|
@ -485,7 +485,6 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
}
|
||||
|
||||
async onFileViewer(fileInfo: FileEventJson) {
|
||||
this.logger.debug('onFileViewer', fileInfo);
|
||||
const result = await this.dialogService.open<
|
||||
FileViewerDialogComponent,
|
||||
FileViewerDialogData,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Injectable, Inject } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { of, Observable } from 'rxjs';
|
||||
import { catchError, exhaustMap, map, tap, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { Actions, ofType, createEffect } from '@ngrx/effects';
|
||||
|
@ -86,19 +86,28 @@ export class Effects {
|
|||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(webLoginSuccess),
|
||||
switchMap(params =>
|
||||
this.nativeService.checkForUpdates().pipe(
|
||||
map((update: boolean) => {
|
||||
if (!update) {
|
||||
this.appAuthenticationService.login(
|
||||
params.loginInfo,
|
||||
params.rememberMe
|
||||
);
|
||||
this.router.navigate(['/messenger']);
|
||||
}
|
||||
}),
|
||||
catchError(error => of(error))
|
||||
)
|
||||
switchMap(
|
||||
params =>
|
||||
new Observable<void>(subscriber => {
|
||||
this.nativeService
|
||||
.checkForUpdates()
|
||||
.then((update: boolean) => {
|
||||
if (!update) {
|
||||
this.appAuthenticationService.login(
|
||||
params.loginInfo,
|
||||
params.rememberMe
|
||||
);
|
||||
this.router.navigate(['/messenger']);
|
||||
}
|
||||
subscriber.next();
|
||||
})
|
||||
.catch(reason => {
|
||||
subscriber.error(reason);
|
||||
})
|
||||
.finally(() => {
|
||||
subscriber.complete();
|
||||
});
|
||||
})
|
||||
)
|
||||
),
|
||||
{ dispatch: false }
|
||||
|
|
|
@ -69,6 +69,113 @@ export class FileUtil {
|
|||
a.remove();
|
||||
}
|
||||
|
||||
static save(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
mimeType: string
|
||||
): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const defaultMime = 'application/octet-stream';
|
||||
const mime = !!mimeType ? mimeType : defaultMime;
|
||||
const blob = new Blob([buffer], { type: mime });
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
// IE10+ : (has Blob, but not a[download] or URL)
|
||||
navigator.msSaveBlob(blob, fileName);
|
||||
resolve(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
const download = (url: string, mode: boolean = false) => {
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
if ('download' in anchor) {
|
||||
// html5 A[download]
|
||||
anchor.href = url;
|
||||
anchor.setAttribute('download', fileName);
|
||||
anchor.className = 'download-js-link';
|
||||
anchor.innerHTML = 'downloading...';
|
||||
anchor.style.display = 'none';
|
||||
document.body.appendChild(anchor);
|
||||
|
||||
setTimeout(() => {
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
resolve(fileName);
|
||||
if (mode) {
|
||||
setTimeout(() => {
|
||||
window.URL.revokeObjectURL(anchor.href);
|
||||
}, 250);
|
||||
}
|
||||
}, 66);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle non-a[download] safari as best we can:
|
||||
if (
|
||||
/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
) {
|
||||
url = url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
if (!window.open(url)) {
|
||||
// popup blocked, offer direct download:
|
||||
if (
|
||||
confirm(
|
||||
'Displaying New Document\n\nUse Save As... to download, then click back to return to this page.'
|
||||
)
|
||||
) {
|
||||
location.href = url;
|
||||
}
|
||||
}
|
||||
resolve(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
// do iframe dataURL download (old ch+FF):
|
||||
const f = document.createElement('iframe');
|
||||
document.body.appendChild(f);
|
||||
|
||||
if (!mode) {
|
||||
// force a mime that will download:
|
||||
url = 'data:' + url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
|
||||
}
|
||||
f.src = url;
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(f);
|
||||
}, 333);
|
||||
};
|
||||
|
||||
if (window.URL) {
|
||||
download(window.URL.createObjectURL(blob), true);
|
||||
} else {
|
||||
if (
|
||||
typeof blob === 'string' ||
|
||||
(blob as any).constructor === this.toString
|
||||
) {
|
||||
try {
|
||||
download(
|
||||
'data:' + mime + ';base64,' + window.btoa((blob as any) as string)
|
||||
);
|
||||
} catch (e) {
|
||||
download(
|
||||
'data:' +
|
||||
mime +
|
||||
';base64,' +
|
||||
encodeURIComponent((blob as any) as string)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = () => {
|
||||
download(fileReader.result as string);
|
||||
};
|
||||
fileReader.readAsDataURL(blob);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static thumbnail(file: File): Promise<File> {
|
||||
return new Promise<File>((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
|
|
|
@ -12,6 +12,7 @@ import { TranslateLoader } from '@ngx-translate/core';
|
|||
import { TranslateLoaderService } from '../translate/browser-loader';
|
||||
import { NotificationService } from '../notification/notification.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FileUtil } from '@ucap-webmessenger/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
@ -34,34 +35,35 @@ export class BrowserNativeService extends NativeService {
|
|||
}
|
||||
closeAllNotify(): void {}
|
||||
|
||||
checkForUpdates(): Observable<boolean> {
|
||||
return new Observable<boolean>(subscriber => {
|
||||
try {
|
||||
subscriber.next(false);
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
} finally {
|
||||
subscriber.complete();
|
||||
}
|
||||
checkForUpdates(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
resolve(false);
|
||||
});
|
||||
}
|
||||
|
||||
showImageViewer(): void {}
|
||||
|
||||
readFile(path: string): Observable<Buffer> {
|
||||
return this.httpClient.get(path, { responseType: 'arraybuffer' }).pipe(
|
||||
map(arrayBuffer => {
|
||||
return Buffer.from(arrayBuffer);
|
||||
})
|
||||
);
|
||||
readFile(path: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
resolve(null);
|
||||
});
|
||||
}
|
||||
|
||||
saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
path?: string
|
||||
): Observable<string> {
|
||||
return this.httpClient.post<string>(path, buffer, {});
|
||||
): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
FileUtil.save(buffer, fileName, mimeType).then(fn => {
|
||||
resolve(fn);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
windowStateChanged(): Observable<WindowState> {
|
||||
|
@ -85,7 +87,6 @@ export class BrowserNativeService extends NativeService {
|
|||
idleStateChanged(): Observable<WindowIdle> {
|
||||
return new Observable<WindowIdle>(subscriber => {
|
||||
try {
|
||||
subscriber.next(WindowIdle.Active);
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
} finally {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { TranslateLoader } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NativeService } from '@ucap-webmessenger/native';
|
||||
import { take, map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
|
@ -18,13 +17,18 @@ export class TranslateLoaderService implements TranslateLoader {
|
|||
* Gets the translations from the server
|
||||
*/
|
||||
public getTranslation(lang: string): Observable<any> {
|
||||
return this.nativeService
|
||||
.readFile(`${this.prefix}${lang}.${this.suffix}`)
|
||||
.pipe(
|
||||
take(1),
|
||||
map(buffer => {
|
||||
return JSON.parse(buffer.toString('utf-8'));
|
||||
return new Observable<any>(subscriber => {
|
||||
this.nativeService
|
||||
.readFile(`${this.prefix}${lang}.${this.suffix}`)
|
||||
.then(buffer => {
|
||||
subscriber.next(JSON.parse(buffer.toString('utf-8')));
|
||||
})
|
||||
);
|
||||
.catch(reason => {
|
||||
subscriber.error(reason);
|
||||
})
|
||||
.finally(() => {
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,14 +47,12 @@ export class ElectronNativeService implements NativeService {
|
|||
this.ipcRenderer.send(NotificationChannel.CloseAllNotify);
|
||||
}
|
||||
|
||||
checkForUpdates(): Observable<boolean> {
|
||||
return new Observable<boolean>(subscriber => {
|
||||
checkForUpdates(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
try {
|
||||
subscriber.next(this.ipcRenderer.sendSync(UpdaterChannel.Check));
|
||||
resolve(this.ipcRenderer.sendSync(UpdaterChannel.Check));
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
} finally {
|
||||
subscriber.complete();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -63,14 +61,12 @@ export class ElectronNativeService implements NativeService {
|
|||
this.ipcRenderer.send(FileChannel.ShowImageViewer);
|
||||
}
|
||||
|
||||
readFile(path: string): Observable<Buffer> {
|
||||
return new Observable<Buffer>(subscriber => {
|
||||
readFile(path: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
try {
|
||||
subscriber.next(this.ipcRenderer.sendSync(FileChannel.ReadFile, path));
|
||||
resolve(this.ipcRenderer.sendSync(FileChannel.ReadFile, path));
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
} finally {
|
||||
subscriber.complete();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -78,22 +74,22 @@ export class ElectronNativeService implements NativeService {
|
|||
saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
path?: string
|
||||
): Observable<string> {
|
||||
return new Observable<string>(subscriber => {
|
||||
): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
subscriber.next(
|
||||
resolve(
|
||||
this.ipcRenderer.sendSync(
|
||||
FileChannel.SaveFile,
|
||||
buffer,
|
||||
fileName,
|
||||
mimeType,
|
||||
path
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
} finally {
|
||||
subscriber.complete();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { TranslateLoader } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NativeService } from '@ucap-webmessenger/native';
|
||||
import { take, map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
|
@ -18,13 +17,18 @@ export class TranslateLoaderService implements TranslateLoader {
|
|||
* Gets the translations from the server
|
||||
*/
|
||||
public getTranslation(lang: string): Observable<any> {
|
||||
return this.nativeService
|
||||
.readFile(`${this.prefix}${lang}.${this.suffix}`)
|
||||
.pipe(
|
||||
take(1),
|
||||
map(buf => {
|
||||
return JSON.parse(buf.toString());
|
||||
return new Observable<any>(subscriber => {
|
||||
this.nativeService
|
||||
.readFile(`${this.prefix}${lang}.${this.suffix}`)
|
||||
.then(buffer => {
|
||||
subscriber.next(JSON.parse(buffer.toString('utf-8')));
|
||||
})
|
||||
);
|
||||
.catch(reason => {
|
||||
subscriber.error(reason);
|
||||
})
|
||||
.finally(() => {
|
||||
subscriber.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,17 @@ export abstract class NativeService {
|
|||
abstract notify(noti: NotificationRequest): void;
|
||||
abstract closeAllNotify(): void;
|
||||
|
||||
abstract checkForUpdates(): Observable<boolean>;
|
||||
abstract checkForUpdates(): Promise<boolean>;
|
||||
|
||||
abstract showImageViewer(): void;
|
||||
|
||||
abstract saveFile(
|
||||
buffer: Buffer,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
path?: string
|
||||
): Observable<string>;
|
||||
abstract readFile(path: string): Observable<Buffer>;
|
||||
): Promise<string>;
|
||||
abstract readFile(path: string): Promise<Buffer>;
|
||||
|
||||
abstract windowStateChanged(): Observable<WindowState>;
|
||||
abstract windowClose(): void;
|
||||
|
|
|
@ -191,12 +191,18 @@ export class ProtocolService {
|
|||
|
||||
responseSubject = new Subject<ProtocolMessage>().pipe(
|
||||
finalize(() => {
|
||||
this.logger.debug(
|
||||
'ProtocolService::pendingRequests.finalize',
|
||||
requestId
|
||||
);
|
||||
|
||||
if (this.pendingRequests.has(requestId)) {
|
||||
this.pendingRequests.delete(requestId);
|
||||
}
|
||||
this.logger.debug(
|
||||
'ProtocolService::pendingRequests.size',
|
||||
this.pendingRequests.size
|
||||
this.pendingRequests.size,
|
||||
this.pendingRequests
|
||||
);
|
||||
})
|
||||
) as Subject<ProtocolMessage>;
|
||||
|
|
|
@ -6,35 +6,35 @@
|
|||
*ngSwitchCase="FileViewerType.Document"
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-document-viewer>
|
||||
<ucap-image-viewer
|
||||
*ngSwitchCase="FileViewerType.Image"
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-image-viewer>
|
||||
<ucap-sound-viewer
|
||||
*ngSwitchCase="FileViewerType.Sound"
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-sound-viewer>
|
||||
<ucap-video-viewer
|
||||
*ngSwitchCase="FileViewerType.Video"
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-video-viewer>
|
||||
<ucap-binary-viewer
|
||||
*ngSwitchDefault
|
||||
[fileInfo]="fileInfo"
|
||||
[fileDownloadUrl]="fileDownloadUrl"
|
||||
(download)="onDownload()"
|
||||
(download)="onDownload($event)"
|
||||
(closed)="onClosedViewer()"
|
||||
></ucap-binary-viewer>
|
||||
</div>
|
||||
|
|
|
@ -3,12 +3,13 @@ import { ucapAnimations } from '../animations';
|
|||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { FileViewerType } from '../types/file-viewer.type';
|
||||
import { FileType } from '@ucap-webmessenger/protocol-file';
|
||||
import { FileDownloadItem } from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-file-viewer',
|
||||
templateUrl: './file-viewer.component.html',
|
||||
styleUrls: ['./file-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class FileViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -18,7 +19,7 @@ export class FileViewerComponent implements OnInit {
|
|||
fileDownloadUrl: string;
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
@ -44,8 +45,8 @@ export class FileViewerComponent implements OnInit {
|
|||
return FileViewerType.Binary;
|
||||
}
|
||||
}
|
||||
onDownload(): void {
|
||||
this.download.emit();
|
||||
onDownload(fileDownloadItem: FileDownloadItem): void {
|
||||
this.download.emit(fileDownloadItem);
|
||||
}
|
||||
|
||||
onClosedViewer(): void {
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
Close
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
<div class="ucap-binary-viewer-body">
|
||||
<div
|
||||
class="ucap-binary-viewer-content-wrapper"
|
||||
|
|
|
@ -2,12 +2,13 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
|||
import { ucapAnimations } from '../../animations';
|
||||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { DeviceType } from '@ucap-webmessenger/core';
|
||||
import { FileDownloadItem } from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-binary-viewer',
|
||||
templateUrl: './binary-viewer.component.html',
|
||||
styleUrls: ['./binary-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class BinaryViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -17,17 +18,20 @@ export class BinaryViewerComponent implements OnInit {
|
|||
fileDownloadUrl: string;
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
onClickDownload(): void {
|
||||
this.download.emit();
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
this.download.emit(this.fileDownloadItem);
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
|
|
|
@ -15,4 +15,9 @@
|
|||
>delete</mat-icon
|
||||
>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
</div>
|
||||
|
|
|
@ -2,12 +2,13 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
|||
import { ucapAnimations } from '../../animations';
|
||||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { DeviceType } from '@ucap-webmessenger/core';
|
||||
import { FileDownloadItem } from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-document-viewer',
|
||||
templateUrl: './document-viewer.component.html',
|
||||
styleUrls: ['./document-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class DocumentViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -17,16 +18,21 @@ export class DocumentViewerComponent implements OnInit {
|
|||
fileDownloadUrl: string;
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
onClickDownload(): void {}
|
||||
onClickDownload(): void {
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
this.download.emit(this.fileDownloadItem);
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
this.closed.emit();
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
<div class="ucap-image-viewer-container">
|
||||
<mat-toolbar class="ucap-image-viewer-header bg-primary-dark">
|
||||
<mat-toolbar class="ucap-image-viewer-header bg-primary-dark">
|
||||
<!--<mat-icon class="ucap-image-viewer-icon">image</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round" class="ucap-image-viewer-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
class="ucap-image-viewer-icon"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<path d="M20.4 14.5L16 10 4 20" /></svg>
|
||||
<path d="M20.4 14.5L16 10 4 20" />
|
||||
</svg>
|
||||
<span class="ucap-image-viewer-title">{{ fileInfo.fileName }}</span>
|
||||
<span class="ucap-image-viewer-spacer"></span>
|
||||
|
||||
|
@ -18,9 +29,20 @@
|
|||
>
|
||||
<!--<mat-icon>settings_overscan</mat-icon>-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<path d="M3.8 3.8l16.4 16.4M20.2 3.8L3.8 20.2M15 3h6v6M9 3H3v6M15 21h6v-6M9 21H3v-6" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M3.8 3.8l16.4 16.4M20.2 3.8L3.8 20.2M15 3h6v6M9 3H3v6M15 21h6v-6M9 21H3v-6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
|
@ -31,7 +53,21 @@
|
|||
aria-label=""
|
||||
>
|
||||
<!--<mat-icon>zoom_out</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="butt" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
|
@ -41,8 +77,17 @@
|
|||
aria-label=""
|
||||
>
|
||||
<!--<mat-icon>zoom_in</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="11" y1="8" x2="11" y2="14"></line>
|
||||
|
@ -58,25 +103,51 @@
|
|||
(click)="onClickDownload()"
|
||||
>
|
||||
<!--<mat-icon>get_app</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5" /></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="stroke-bar"></span>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
class="ucap-image-viewer-action btn-close"
|
||||
matTooltip="뷰어닫기"
|
||||
matTooltip="뷰어닫기"
|
||||
(click)="onClickClose()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
<div class="ucap-image-viewer-body">
|
||||
<div
|
||||
class="ucap-image-viewer-image-wrapper"
|
||||
|
|
|
@ -2,13 +2,16 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
|||
import { ucapAnimations } from '../../animations';
|
||||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { DeviceType } from '@ucap-webmessenger/core';
|
||||
import { CommonApiService } from '@ucap-webmessenger/api-common';
|
||||
import {
|
||||
CommonApiService,
|
||||
FileDownloadItem,
|
||||
} from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-image-viewer',
|
||||
templateUrl: './image-viewer.component.html',
|
||||
styleUrls: ['./image-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class ImageViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -21,14 +24,17 @@ export class ImageViewerComponent implements OnInit {
|
|||
closed = new EventEmitter<void>();
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
onClickDownload(): void {
|
||||
this.download.emit();
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
this.download.emit(this.fileDownloadItem);
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
Close
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
<div class="ucap-sound-viewer-body">
|
||||
<div
|
||||
class="ucap-sound-viewer-sound-icon"
|
||||
|
|
|
@ -5,17 +5,18 @@ import {
|
|||
Output,
|
||||
EventEmitter,
|
||||
ViewChild,
|
||||
ElementRef
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { ucapAnimations } from '../../animations';
|
||||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { MatSlider, MatSliderChange } from '@angular/material';
|
||||
import { FileDownloadItem } from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-sound-viewer',
|
||||
templateUrl: './sound-viewer.component.html',
|
||||
styleUrls: ['./sound-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class SoundViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -25,7 +26,7 @@ export class SoundViewerComponent implements OnInit {
|
|||
fileDownloadUrl: string;
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
@ -41,6 +42,7 @@ export class SoundViewerComponent implements OnInit {
|
|||
currentTime = 0;
|
||||
volume = 0.1;
|
||||
loading = false;
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
@ -90,7 +92,8 @@ export class SoundViewerComponent implements OnInit {
|
|||
}
|
||||
|
||||
onClickDownload(): void {
|
||||
this.download.emit();
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
this.download.emit(this.fileDownloadItem);
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
<div class="ucap-video-viewer-container">
|
||||
<mat-toolbar color="accent" class="ucap-video-viewer-header">
|
||||
<!--<mat-icon class="ucap-video-viewer-icon">video_label</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round" class="ucap-video-viewer-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
class="ucap-video-viewer-icon"
|
||||
>
|
||||
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
|
||||
<line x1="7" y1="2" x2="7" y2="22"></line>
|
||||
<line x1="17" y1="2" x2="17" y2="22"></line>
|
||||
|
@ -23,9 +33,20 @@
|
|||
(click)="onClickDownload()"
|
||||
>
|
||||
<!--<mat-icon>get_app</mat-icon>-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="stroke-bar"></span>
|
||||
|
@ -33,16 +54,30 @@
|
|||
mat-icon-button
|
||||
color="warn"
|
||||
class="ucap-image-viewer-action btn-close"
|
||||
matTooltip="뷰어닫기"
|
||||
matTooltip="뷰어닫기"
|
||||
(click)="onClickClose()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="butt" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="butt"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar
|
||||
*ngIf="fileDownloadItem && fileDownloadItem.downloadingProgress$"
|
||||
mode="determinate"
|
||||
[value]="fileDownloadItem.downloadingProgress$ | async"
|
||||
></mat-progress-bar>
|
||||
<div class="ucap-video-viewer-body">
|
||||
<div
|
||||
class="ucap-video-viewer-video-icon"
|
||||
|
|
|
@ -5,17 +5,18 @@ import {
|
|||
Output,
|
||||
EventEmitter,
|
||||
ViewChild,
|
||||
ElementRef
|
||||
ElementRef,
|
||||
} from '@angular/core';
|
||||
import { ucapAnimations } from '../../animations';
|
||||
import { FileEventJson } from '@ucap-webmessenger/protocol-event';
|
||||
import { MatSlider, MatSliderChange } from '@angular/material';
|
||||
import { FileDownloadItem } from '@ucap-webmessenger/api-common';
|
||||
|
||||
@Component({
|
||||
selector: 'ucap-video-viewer',
|
||||
templateUrl: './video-viewer.component.html',
|
||||
styleUrls: ['./video-viewer.component.scss'],
|
||||
animations: ucapAnimations
|
||||
animations: ucapAnimations,
|
||||
})
|
||||
export class VideoViewerComponent implements OnInit {
|
||||
@Input()
|
||||
|
@ -25,7 +26,7 @@ export class VideoViewerComponent implements OnInit {
|
|||
fileDownloadUrl: string;
|
||||
|
||||
@Output()
|
||||
download = new EventEmitter<void>();
|
||||
download = new EventEmitter<FileDownloadItem>();
|
||||
|
||||
@Output()
|
||||
closed = new EventEmitter<void>();
|
||||
|
@ -41,6 +42,7 @@ export class VideoViewerComponent implements OnInit {
|
|||
currentTime = 0;
|
||||
volume = 0.1;
|
||||
loading = false;
|
||||
fileDownloadItem: FileDownloadItem;
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
@ -90,7 +92,8 @@ export class VideoViewerComponent implements OnInit {
|
|||
}
|
||||
|
||||
onClickDownload(): void {
|
||||
this.download.emit();
|
||||
this.fileDownloadItem = new FileDownloadItem();
|
||||
this.download.emit(this.fileDownloadItem);
|
||||
}
|
||||
|
||||
onClickClose(): void {
|
||||
|
|
Loading…
Reference in New Issue
Block a user