0710 sync

This commit is contained in:
Park Byung Eun 2020-07-11 19:57:44 +09:00
parent cbe1ddbd29
commit bf63e11021
239 changed files with 12389 additions and 2697 deletions

View File

@ -1074,44 +1074,6 @@
}
}
},
"native-browser": {
"projectType": "library",
"root": "projects/native-browser",
"sourceRoot": "projects/native-browser/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/native-browser/tsconfig.lib.json",
"project": "projects/native-browser/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/native-browser/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/native-browser/src/test.ts",
"tsConfig": "projects/native-browser/tsconfig.spec.json",
"karmaConfig": "projects/native-browser/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/native-browser/tsconfig.lib.json",
"projects/native-browser/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"]
}
}
}
},
"store-authentication": {
"projectType": "library",
"root": "projects/store-authentication",

760
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,8 @@
"build:pi": "node ./scripts/build.js pi",
"build:core": "node ./scripts/build.js core",
"build:logger": "node ./scripts/build.js logger",
"build:native:all": "npm-run-all -s build:native build:native-browser",
"build:native:all": "npm-run-all -s build:native",
"build:native": "node ./scripts/build.js native",
"build:native-browser": "node ./scripts/build.js native-browser",
"build:protocol:all": "npm-run-all -s build:protocol build:protocol-authentication build:protocol-buddy build:protocol-event build:protocol-file build:protocol-group build:protocol-status build:protocol-info build:protocol-inner build:protocol-option build:protocol-ping build:protocol-query build:protocol-room build:protocol-service build:protocol-sync build:protocol-umg",
"build:protocol": "node ./scripts/build.js protocol",
"build:protocol-authentication": "node ./scripts/build.js protocol-authentication",
@ -65,9 +64,8 @@
"publish:pi": "cd ./dist/pi && npm publish",
"publish:core": "cd ./dist/core && npm publish",
"publish:logger": "cd ./dist/logger && npm publish",
"publish:native:all": "npm-run-all -s publish:native publish:native-browser",
"publish:native:all": "npm-run-all -s publish:native",
"publish:native": "cd ./dist/native && npm publish",
"publish:native-browser": "cd ./dist/native-browser && npm publish",
"publish:protocol:all": "npm-run-all -s publish:protocol publish:protocol-authentication publish:protocol-buddy publish:protocol-event publish:protocol-file publish:protocol-group publish:protocol-status publish:protocol-info publish:protocol-inner publish:protocol-option publish:protocol-ping publish:protocol-query publish:protocol-room publish:protocol-service publish:protocol-sync publish:protocol-umg",
"publish:protocol": "cd ./dist/protocol && npm publish",
"publish:protocol-authentication": "cd ./dist/protocol-authentication && npm publish",
@ -110,35 +108,33 @@
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"dependencies": {
"@storybook/addon-knobs": "^5.3.18",
"tslib": "^1.10.0"
},
"dependencies": {},
"devDependencies": {
"@angular-devkit/build-angular": "^0.900.6",
"@angular-devkit/build-ng-packagr": "^0.900.6",
"@angular/animations": "^9.0.6",
"@angular/cdk": "^9.1.2",
"@angular/cli": "^9.0.6",
"@angular/common": "^9.0.6",
"@angular/compiler": "^9.0.6",
"@angular/compiler-cli": "^9.0.6",
"@angular/core": "^9.0.6",
"@angular/flex-layout": "^9.0.0-beta.29",
"@angular/forms": "^9.0.6",
"@angular/language-service": "^9.0.6",
"@angular/material": "^9.1.2",
"@angular/material-moment-adapter": "^9.1.2",
"@angular/platform-browser": "^9.0.6",
"@angular/platform-browser-dynamic": "^9.0.6",
"@angular/router": "^9.0.6",
"@angular/animations": "^9.1.11",
"@angular/cdk": "^9.2.4",
"@angular/cli": "^9.1.9",
"@angular/common": "^9.1.11",
"@angular/compiler": "^9.1.11",
"@angular/compiler-cli": "^9.1.11",
"@angular/core": "^9.1.11",
"@angular/flex-layout": "^9.0.0-beta.31",
"@angular/forms": "^9.1.11",
"@angular/language-service": "^9.1.11",
"@angular/material": "^9.2.4",
"@angular/material-moment-adapter": "^9.2.4",
"@angular/platform-browser": "^9.1.11",
"@angular/platform-browser-dynamic": "^9.1.11",
"@angular/router": "^9.1.11",
"@babel/core": "^7.9.0",
"@ngrx/effects": "^9.0.0",
"@ngrx/entity": "^9.0.0",
"@ngrx/router-store": "^9.0.0",
"@ngrx/store": "^9.0.0",
"@ngrx/store-devtools": "^9.0.0",
"@ngrx/effects": "^9.2.0",
"@ngrx/entity": "^9.2.0",
"@ngrx/router-store": "^9.2.0",
"@ngrx/store": "^9.2.0",
"@ngrx/store-devtools": "^9.2.0",
"@storybook/addon-actions": "^5.3.18",
"@storybook/addon-knobs": "^5.3.18",
"@storybook/addon-links": "^5.3.18",
"@storybook/addon-notes": "^5.3.18",
"@storybook/addons": "^5.3.18",
@ -148,15 +144,14 @@
"@types/moment-timezone": "^0.5.12",
"@types/node": "^12.12.30",
"@ucap/api": "~0.0.1",
"@ucap/api-common": "~0.0.5",
"@ucap/api-common": "~0.0.7",
"@ucap/api-external": "~0.0.2",
"@ucap/api-message": "~0.0.1",
"@ucap/api-prompt": "~0.0.1",
"@ucap/api-public": "~0.0.1",
"@ucap/core": "~0.0.6",
"@ucap/core": "~0.0.14",
"@ucap/logger": "~0.0.12",
"@ucap/native": "~0.0.1",
"@ucap/native-browser": "~0.0.1",
"@ucap/native": "~0.0.19",
"@ucap/ng-api-common": "file:pack/ucap-ng-api-common-0.0.1.tgz",
"@ucap/ng-api-external": "file:pack/ucap-ng-api-external-0.0.1.tgz",
"@ucap/ng-api-message": "file:pack/ucap-ng-api-message-0.0.1.tgz",
@ -165,8 +160,7 @@
"@ucap/ng-core": "file:pack/ucap-ng-core-0.0.7.tgz",
"@ucap/ng-i18n": "file:pack/ucap-ng-i18n-0.0.6.tgz",
"@ucap/ng-logger": "file:pack/ucap-ng-logger-0.0.2.tgz",
"@ucap/ng-native": "file:pack/ucap-ng-native-0.0.1.tgz",
"@ucap/ng-native-browser": "file:pack/ucap-ng-native-browser-0.0.1.tgz",
"@ucap/ng-native": "file:pack/ucap-ng-native-0.0.5.tgz",
"@ucap/ng-pi": "file:pack/ucap-ng-pi-0.0.1.tgz",
"@ucap/ng-protocol": "file:pack/ucap-ng-protocol-0.0.3.tgz",
"@ucap/ng-protocol-authentication": "file:pack/ucap-ng-protocol-authentication-0.0.3.tgz",
@ -184,37 +178,37 @@
"@ucap/ng-protocol-status": "file:pack/ucap-ng-protocol-status-0.0.3.tgz",
"@ucap/ng-protocol-sync": "file:pack/ucap-ng-protocol-sync-0.0.3.tgz",
"@ucap/ng-protocol-umg": "file:pack/ucap-ng-protocol-umg-0.0.3.tgz",
"@ucap/ng-store-authentication": "file:pack/ucap-ng-store-authentication-0.0.11.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.19.tgz",
"@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.14.tgz",
"@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.8.tgz",
"@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.24.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.25.tgz",
"@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.13.tgz",
"@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.33.tgz",
"@ucap/ng-store-authentication": "file:pack/ucap-ng-store-authentication-0.0.14.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.66.tgz",
"@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.22.tgz",
"@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.20.tgz",
"@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.97.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.29.tgz",
"@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.72.tgz",
"@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.78.tgz",
"@ucap/ng-ui-material": "file:pack/ucap-ng-ui-material-0.0.4.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.84.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.202.tgz",
"@ucap/ng-ui-skin-default": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz",
"@ucap/ng-web-socket": "file:pack/ucap-ng-web-socket-0.0.2.tgz",
"@ucap/ng-web-storage": "file:pack/ucap-ng-web-storage-0.0.3.tgz",
"@ucap/pi": "~0.0.5",
"@ucap/pi": "~0.0.8",
"@ucap/protocol": "~0.0.11",
"@ucap/protocol-authentication": "~0.0.5",
"@ucap/protocol-buddy": "~0.0.5",
"@ucap/protocol-event": "~0.0.5",
"@ucap/protocol-file": "~0.0.5",
"@ucap/protocol-event": "~0.0.6",
"@ucap/protocol-file": "~0.0.6",
"@ucap/protocol-group": "~0.0.5",
"@ucap/protocol-info": "~0.0.6",
"@ucap/protocol-info": "~0.0.9",
"@ucap/protocol-inner": "~0.0.4",
"@ucap/protocol-option": "~0.0.7",
"@ucap/protocol-ping": "~0.0.4",
"@ucap/protocol-query": "~0.0.5",
"@ucap/protocol-room": "~0.0.6",
"@ucap/protocol-room": "~0.0.7",
"@ucap/protocol-service": "~0.0.4",
"@ucap/protocol-status": "~0.0.5",
"@ucap/protocol-sync": "~0.0.4",
"@ucap/protocol-sync": "~0.0.6",
"@ucap/protocol-umg": "~0.0.5",
"@ucap/ui-scss": "~0.0.3",
"@ucap/ui-scss": "~0.0.5",
"@ucap/web-socket": "~0.0.5",
"@ucap/web-storage": "~0.0.5",
"autolinker": "^3.13.0",

View File

@ -1,24 +0,0 @@
# NativeBrowser
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2.
## Code scaffolding
Run `ng generate component component-name --project native-browser` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project native-browser`.
> Note: Don't forget to add `--project native-browser` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build native-browser` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build native-browser`, go to the dist folder `cd dist/native-browser` and run `npm publish`.
## Running unit tests
Run `ng test native-browser` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/native-browser'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -1,11 +0,0 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/native-browser",
"lib": {
"entryFile": "src/public-api.ts",
"umdModuleIds": {
"@ucap/native-browser": "@ucap/native-browser",
"@ucap/ng-core": "@ucap/ng-core"
}
}
}

View File

@ -1,16 +0,0 @@
{
"name": "@ucap/ng-native-browser",
"version": "0.0.1",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
"peerDependencies": {
"@angular/common": "^9.0.2",
"@angular/core": "^9.0.2",
"@ucap/native-browser": "~0.0.1",
"@ucap/ng-core": "~0.0.1",
"axios": "^0.19.2",
"rxjs": "~6.5.4",
"tslib": "^1.10.0"
}
}

View File

@ -1,16 +0,0 @@
import { Injectable, Inject } from '@angular/core';
import { AxiosInstance } from 'axios';
import { BrowserNativeService as UcapBrowserNativeService } from '@ucap/native-browser';
import { AXIOS_INSTANCE } from '@ucap/ng-core';
@Injectable({
providedIn: 'root'
})
export class BrowserNativeService extends UcapBrowserNativeService {
constructor(@Inject(AXIOS_INSTANCE) axios: AxiosInstance) {
super(axios);
}
}

View File

@ -1,5 +0,0 @@
/*
* Public API Surface of native-browser
*/
export * from './lib/services/browser-native.service';

View File

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -1,23 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -1,6 +0,0 @@
{
"extends": "./tsconfig.lib.json",
"angularCompilerOptions": {
"enableIvy": false
}
}

View File

@ -1,17 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -1,17 +0,0 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}

View File

@ -2,6 +2,11 @@
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/native",
"lib": {
"entryFile": "src/public-api.ts"
"entryFile": "src/public-api.ts",
"umdModuleIds": {
"@ucap/core": "@ucap/core",
"@ucap/native": "@ucap/native",
"@ucap/ng-core": "@ucap/ng-core"
}
}
}

View File

@ -1,13 +1,15 @@
{
"name": "@ucap/ng-native",
"version": "0.0.1",
"version": "0.0.5",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
"peerDependencies": {
"@angular/common": "^9.0.2",
"@angular/core": "^9.0.2",
"@ucap/core": "~0.0.1",
"@ucap/native": "~0.0.1",
"axios": "^0.19.2",
"rxjs": "~6.5.4",
"tslib": "^1.10.0"
}

View File

@ -0,0 +1,666 @@
import { Subject, BehaviorSubject } from 'rxjs';
import { AxiosInstance } from 'axios';
import { StatusCode, FileUtil } from '@ucap/core';
import {
NativeService,
UpdateCheckConfig,
UpdateInfo,
NativeType,
WindowState,
WindowIdle,
NotificationRequest,
NotificationType
} from '@ucap/native';
import { NotificationService } from './notification.service';
export class BrowserNativeService extends NativeService {
// tslint:disable-next-line: variable-name
private _notificationService: NotificationService;
// tslint:disable-next-line: variable-name
private _idle_checker: WindowIdleChecker;
constructor(private axios: AxiosInstance) {
super();
this._notificationService = new NotificationService();
}
platform_nativeType(): Promise<NativeType> {
return new Promise<any>((resolve, reject) => {
try {
resolve(NativeType.Browser);
} catch (error) {
reject(error);
}
});
}
platform_networkInfo(): Promise<any> {
return new Promise<any>((resolve, reject) => {
try {
resolve([{ ip: 'Browser', mac: 'browser' }]);
} catch (error) {
reject(error);
}
});
}
platform_execute(executableName: string): Promise<number> {
return new Promise<number>((resolve, reject) => {
try {
resolve(-1);
} catch (error) {
reject(error);
}
});
}
platform_openDefaultBrowser(
url: string,
options?: {
name?: string;
features?: string;
replace?: boolean;
}
): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
const name = !!options && !!options.name ? options.name : null;
const features =
!!options && !!options.features ? options.features : null;
window.open(url, name, features);
resolve();
} catch (error) {
reject(error);
}
});
}
platform_readFromClipboard(): Promise<{
text?: string;
rtf?: string;
html?: string;
image?: Buffer;
imageDataUrl?: string;
}> {
return new Promise<{
text?: string;
rtf?: string;
html?: string;
image?: Buffer;
imageDataUrl?: string;
}>((resolve, reject) => {
try {
navigator.permissions
.query({ name: 'clipboard-read' as PermissionName })
.then((result) => {
if ('granted' === result.state || 'prompt' === result.state) {
navigator.clipboard
.readText()
.then((value) => {
resolve({ text: value });
})
.catch((reason) => {
reject(reason);
});
}
})
.catch((reason) => {
reject(reason);
});
} catch (error) {
reject(error);
}
});
}
file_save(
buffer: Buffer,
fileName: string,
mimeType: string,
path?: string
): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
FileUtil.save(buffer, fileName, mimeType)
.then((fn) => {
resolve(fn);
})
.catch((reason) => {
reject(reason);
});
} catch (error) {
reject(error);
}
});
}
file_read(path: string): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
try {
this.axios
.get(path, { responseType: 'arraybuffer' })
.then((res) => {
resolve(Buffer.from(res.data));
})
.catch((reason) => reject(reason));
} catch (error) {
reject(error);
}
});
}
file_openFolder(folderPath?: string, make?: boolean): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
resolve(false);
} catch (error) {
reject(error);
}
});
}
file_openItem(filePath?: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
resolve(false);
} catch (error) {
reject(error);
}
});
}
file_path(
name: 'home' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos',
...appendPaths: string[]
): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
resolve(undefined);
} catch (error) {
reject(error);
}
});
}
file_selectForOpen(option: {
title?: string;
defaultPath?: string;
filters?: {
extensions: string[];
name: string;
}[];
properties?: Array<
| 'openFile'
| 'openDirectory'
| 'multiSelections'
| 'showHiddenFiles'
| 'createDirectory'
| 'promptToCreate'
| 'noResolveAliases'
| 'treatPackageAsDirectory'
>;
message?: string;
}): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
resolve(undefined);
} catch (error) {
reject(error);
}
});
}
file_selectForSave(option: {
title?: string;
defaultPath?: string;
filters?: {
extensions: string[];
name: string;
}[];
message?: string;
}): Promise<{
canceled: boolean;
filePath: string;
}> {
return new Promise<{
canceled: boolean;
filePath: string;
}>((resolve, reject) => {
try {
resolve(undefined);
} catch (error) {
reject(error);
}
});
}
window_onState$(): BehaviorSubject<WindowState> {
return super.window_onState$();
}
window_onFocus$(): BehaviorSubject<boolean> {
if (!this._window_onFocusSubject) {
// tslint:disable-next-line: variable-name
const __this = this;
const onFocus = (event: Event) => {
__this._window_onFocusSubject.next(true);
};
const onBlur = (event: Event) => {
__this._window_onFocusSubject.next(false);
};
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
super.window_onFocus$().subscribe(
(focus) => {},
(error) => {},
() => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
}
);
}
return super.window_onFocus$();
}
window_close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
window_minimize(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
window_maximize(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
idle_startCheck(limitTime: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
if (!!this._idle_onStateSubject) {
return;
}
this._idle_checker = new WindowIdleChecker({
limitTime,
onIdle: () => {
if (!!this._idle_onStateSubject) {
this._idle_onStateSubject.next(WindowIdle.Idle);
}
},
onActive: () => {
if (!!this._idle_onStateSubject) {
this._idle_onStateSubject.next(WindowIdle.Active);
}
},
onHide: () => {
if (!!this._idle_onStateSubject) {
this._idle_onStateSubject.next(WindowIdle.Idle);
}
},
onShow: () => {
if (!!this._idle_onStateSubject) {
this._idle_onStateSubject.next(WindowIdle.Active);
}
}
});
this._idle_checker.start();
resolve();
} catch (error) {
reject(error);
}
});
}
idle_onState$(): BehaviorSubject<WindowIdle> {
return super.idle_onState$();
}
idle_stopCheck(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
if (!!this._idle_checker) {
this._idle_checker.stop();
this._idle_checker = undefined;
}
if (!!this._idle_onStateSubject) {
this._idle_onStateSubject.complete();
this._idle_onStateSubject = undefined;
}
resolve();
} catch (error) {
reject(error);
}
});
}
idle_changeLimitTime(limitTime: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
if (!this._idle_checker) {
return;
}
this._idle_checker.changeLimitTime(limitTime);
resolve();
} catch (error) {
reject(error);
}
});
}
app_version(): Promise<string> {
return new Promise<any>((resolve, reject) => {
try {
resolve('');
} catch (error) {
reject(error);
}
});
}
app_postInit(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_postLogin(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
this._notificationService.requestPermission();
resolve();
} catch (error) {
reject(error);
}
});
}
app_postLogout(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_postDestroy(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_changeAutoLaunch(autoLaunch: boolean): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
resolve(true);
} catch (error) {
reject(error);
}
});
}
app_showNotify(req: NotificationRequest): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
this._notificationService.notify(req, () => {
window.focus();
switch (req.type) {
case NotificationType.Event:
if (!!this._chat_onOpenSubject) {
this._chat_onOpenSubject.next(req.seq);
}
break;
case NotificationType.Message:
if (!!this._message_onOpenSubject) {
this._message_onOpenSubject.next(req.seq);
}
break;
default:
break;
}
});
resolve();
} catch (error) {
reject(error);
}
});
}
app_closeAllNotify(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_checkForUpdates(currentVersion: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_startCheckForUpdate(config: UpdateCheckConfig): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_onUpdate$(): BehaviorSubject<UpdateInfo> {
return super.app_onUpdate$();
}
app_stopCheckForUpdate(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_applyInstantUpdates(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
resolve();
} catch (error) {
reject(error);
}
});
}
app_exit(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
window.close();
resolve();
} catch (error) {
reject(error);
}
});
}
app_onLogout$(): Subject<void> {
return super.app_onLogout$();
}
app_onStatus$(): BehaviorSubject<StatusCode> {
return super.app_onStatus$();
}
app_onShowSetting$(): Subject<void> {
return super.app_onShowSetting$();
}
chat_onOpen$(): BehaviorSubject<string> {
return super.chat_onOpen$();
}
message_onOpen$(): BehaviorSubject<string> {
return super.message_onOpen$();
}
}
interface WindowIdleCheckerConfig {
limitTime?: number;
events?: string[];
onIdle?: () => void;
onActive?: () => void;
visibilityEvents?: string[];
onHide?: () => void;
onShow?: () => void;
}
const defaultWindowIdleCheckerConfig: WindowIdleCheckerConfig = {
limitTime: 5 * 60 * 1000,
events: ['mousemove', 'keydown', 'mousedown', 'touchstart'],
onIdle: () => {},
onActive: () => {},
onHide: () => {},
onShow: () => {},
visibilityEvents: [
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'msvisibilitychange'
]
};
const WEBKIT_HIDDEN = 'webkitHidden';
const MOZ_HIDDEN = 'mozHidden';
const MS_HIDDEN = 'msHidden';
class WindowIdleChecker {
// tslint:disable-next-line: variable-name
private _timerId: any;
// tslint:disable-next-line: variable-name
private _status: WindowIdle;
// tslint:disable-next-line: variable-name
private _visible: boolean;
// tslint:disable-next-line: variable-name
private _started: boolean;
constructor(
// tslint:disable-next-line: variable-name
private _config: WindowIdleCheckerConfig
) {
this._config = Object.assign({}, defaultWindowIdleCheckerConfig, _config);
this._status = WindowIdle.Active;
this._visible = true;
}
start() {
if (!!this._timerId) {
clearInterval(this._timerId);
}
this._timerId = setInterval(() => {
if (WindowIdle.Active === this._status) {
this._status = WindowIdle.Idle;
this._config.onIdle();
}
}, this._config.limitTime);
if (!this._started) {
this._started = true;
}
this._addEventListerner(this._config.events, this._onActive);
if (!!this._config.onShow || !!this._config.onHide) {
this._addEventListerner(
this._config.visibilityEvents,
this._onVisibility
);
}
}
stop() {
if (!!this._timerId) {
clearInterval(this._timerId);
this._timerId = undefined;
}
if (!!this._started) {
this._removeEventListerner(this._config.events, this._onActive);
if (!!this._config.onShow || !!this._config.onHide) {
this._removeEventListerner(
this._config.visibilityEvents,
this._onVisibility
);
}
}
}
changeLimitTime(limitTime: number) {
this._config = {
...this._config,
limitTime
};
this.start();
}
private _onActive(event: Event) {
if (WindowIdle.Idle === this._status) {
this._status = WindowIdle.Active;
this._config.onActive();
}
}
private _onVisibility(event: Event) {
if (
!!document.hidden ||
!!document[WEBKIT_HIDDEN] ||
!!document[MOZ_HIDDEN] ||
!!document[MS_HIDDEN]
) {
if (this._visible) {
this._visible = false;
this._config.onHide();
}
} else {
if (!this._visible) {
this._visible = true;
this._config.onShow();
}
}
}
private _addEventListerner(
events: string[],
callback: (event: Event) => void
) {
events.forEach((e) => {
window.addEventListener(e, callback.bind(this));
});
}
private _removeEventListerner(
events: string[],
callback: (event: Event) => void
) {
events.forEach((e) => {
window.removeEventListener(e, callback.bind(this));
});
}
}

View File

@ -0,0 +1,44 @@
import { NotificationRequest } from '@ucap/native';
export class NotificationService {
notificationPermission: NotificationPermission;
constructor() {
this.notificationPermission = this.isSupported() ? 'default' : 'denied';
}
public isSupported(): boolean {
return 'Notification' in window;
}
requestPermission(): void {
const self = this;
if ('Notification' in window) {
Notification.requestPermission().then((result) => {
self.notificationPermission = result;
});
}
}
notify(noti: NotificationRequest, click?: () => void) {
if (!this.isSupported()) {
return;
}
const notification = new Notification(noti.title, {
body: noti.contents,
icon: noti.image || 'assets/images/img_nophoto_50.png'
});
notification.onclick = (e) => {
console.log('notification.onclick');
if (!!click) {
click();
}
};
notification.onclose = (e) => {
console.log('notification.onclose');
};
notification.onerror = (e) => {
console.log('notification.onerror');
};
}
}

View File

@ -2,4 +2,7 @@
* Public API Surface of native
*/
export * from './lib/services/notification.service';
export * from './lib/services/browser-native.service';
export * from './lib/types/token';

View File

@ -18,7 +18,8 @@
"@ucap/ng-protocol-authentication": "@ucap/ng-protocol-authentication",
"@ucap/ng-protocol-info": "@ucap/ng-protocol-info",
"@ucap/ng-protocol-option": "@ucap/ng-protocol-option",
"@ucap/ng-protocol-query": "@ucap/ng-protocol-query"
"@ucap/ng-protocol-query": "@ucap/ng-protocol-query",
"@ucap/ng-store-organization": "@ucap/ng-store-organization"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-store-authentication",
"version": "0.0.11",
"version": "0.0.14",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -23,6 +23,7 @@
"@ucap/ng-protocol-option": "~0.0.1",
"@ucap/ng-protocol-query": "~0.0.1",
"@ucap/ng-protocol-info": "~0.0.1",
"@ucap/ng-store-organization": "~0.0.1",
"rxjs": "~6.5.4",
"tslib": "^1.10.0"
}

View File

@ -7,7 +7,6 @@ import {
LoginResponse,
LogoutResponse
} from '@ucap/protocol-authentication';
import { UserResponse, UserRequest } from '@ucap/protocol-info';
/**
* request of web login
@ -107,29 +106,3 @@ export const sessionDestroyed = createAction(
'[ucap::authentication::login] session Destroyed',
props<{ error: any }>()
);
/**
* info user request
*/
export const infoUser = createAction(
'[ucap::authentication::login] Info User',
props<{ req: UserRequest }>()
);
/**
* Success of info user request
*/
export const infoUserSuccess = createAction(
'[ucap::authentication::login] Info User Success',
props<{
res: UserResponse;
}>()
);
/**
* Failure of info user request
*/
export const infoUserFailure = createAction(
'[ucap::authentication::login] Info User Failure',
props<{ error: any }>()
);

View File

@ -1,22 +1,16 @@
import { of } from 'rxjs';
import { catchError, map, switchMap, exhaustMap } from 'rxjs/operators';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { PiService } from '@ucap/ng-pi';
import { AuthenticationProtocolService } from '@ucap/ng-protocol-authentication';
import { InfoProtocolService } from '@ucap/ng-protocol-info';
import {
logout,
logoutSuccess,
infoUser,
infoUserSuccess,
infoUserFailure
} from './actions';
import { UserResponse } from '@ucap/protocol-info';
import { UserActions } from '@ucap/ng-store-organization';
import { logout, logoutSuccess, loginSuccess } from './actions';
@Injectable()
export class Effects {
@ -32,27 +26,37 @@ export class Effects {
);
});
infoUser$ = createEffect(() =>
loginSuccessForOrganizationUserInit$ = createEffect(
() =>
this.actions$.pipe(
ofType(infoUser),
map((action) => action.req),
exhaustMap((req) =>
this.infoProtocolService.user(req).pipe(
map((res: UserResponse) => {
return infoUserSuccess({
res
});
}),
catchError((error) => of(infoUserFailure({ error })))
)
)
)
ofType(loginSuccess),
map((params) => params.res),
tap((loginRes) => {
this.store.dispatch(
UserActions.init({
user: {
info: loginRes.userInfo,
companyCode: loginRes.companyCode,
departmentCode: loginRes.departmentCode,
statusMessage1: loginRes.statusMessage1,
statusMessage2: loginRes.statusMessage2,
statusMessage3: loginRes.statusMessage3,
madn: loginRes.madn,
hardPhoneSadn: loginRes.hardPhoneSadn,
fmcSadn: loginRes.fmcSadn,
pbxIndex: loginRes.pbxIndex,
talkWithMeBotSeq: loginRes.talkWithMeBotSeq
}
})
);
})
),
{ dispatch: false }
);
constructor(
private actions$: Actions,
private piService: PiService,
private authenticationProtocolService: AuthenticationProtocolService,
private infoProtocolService: InfoProtocolService
private store: Store<any>,
private authenticationProtocolService: AuthenticationProtocolService
) {}
}

View File

@ -1,8 +1,7 @@
import { createReducer, on } from '@ngrx/store';
import { initialState } from './state';
import { loginSuccess, logoutSuccess, infoUserSuccess } from './actions';
import { UserInfoUpdateType } from '@ucap/protocol-info';
import { loginSuccess, logoutSuccess } from './actions';
export const reducer = createReducer(
initialState,
@ -17,41 +16,5 @@ export const reducer = createReducer(
return {
...initialState
};
}),
on(infoUserSuccess, (state, action) => {
let loginRes = {
...state.loginRes
};
switch (action.res.type) {
case UserInfoUpdateType.Image:
loginRes = {
...loginRes
};
break;
case UserInfoUpdateType.Intro:
loginRes = {
...loginRes,
userInfo: {
...loginRes.userInfo,
intro: action.res.info
}
};
break;
case UserInfoUpdateType.TelephoneVisible:
loginRes = {
...loginRes
};
break;
default:
break;
}
return {
...state,
loginRes: {
...loginRes
}
};
})
);

View File

@ -8,13 +8,20 @@
"@ngrx/store": "@ngrx/store",
"@ngrx/entity": "@ngrx/entity",
"@ngrx/effects": "@ngrx/effects",
"@ucap/api": "@ucap/api",
"@ucap/pi": "@ucap/pi",
"@ucap/protocol-event": "@ucap/protocol-event",
"@ucap/protocol-file": "@ucap/protocol-file",
"@ucap/protocol-info": "@ucap/protocol-info",
"@ucap/protocol-room": "@ucap/protocol-room",
"@ucap/protocol-sync": "@ucap/protocol-sync",
"@ucap/ng-i18n": "@ucap/ng-i18n",
"@ucap/ng-api-common": "@ucap/ng-api-common",
"@ucap/ng-protocol-room": "@ucap/ng-protocol-room",
"@ucap/ng-protocol-event": "@ucap/ng-protocol-event",
"@ucap/ng-protocol-file": "@ucap/ng-protocol-file",
"@ucap/ng-protocol-sync": "@ucap/ng-protocol-sync",
"@ucap/ng-store-organization": "@ucap/ng-store-organization",
"@ucap/ng-store-authentication": "@ucap/ng-store-authentication"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-store-chat",
"version": "0.0.19",
"version": "0.0.66",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -1,5 +1,7 @@
import { createAction, props } from '@ngrx/store';
import { DeviceType } from '@ucap/core';
import {
Info,
InfoRequest as EventInfoRequest,
@ -16,14 +18,17 @@ import {
CancelNotification,
DelNotification,
EventJson,
ReadResponse
ReadResponse,
SendRequest
} from '@ucap/protocol-event';
import {
InfoRequest as FileInfoRequest,
InfoResponse as FileInfoResponse,
FileDownloadInfo,
FileInfo
FileInfo,
DownCheckRequest,
DownCheckResponse
} from '@ucap/protocol-file';
/**
@ -51,6 +56,13 @@ export const eventsFailure = createAction(
'[ucap::chat::chatting] events Failure',
props<{ roomId: string; error: any }>()
);
/**
* retrieve list of event
*/
export const moreEvents = createAction(
'[ucap::chat::chatting] events more',
props<{ roomId: string }>()
);
/**
* retrieve list of file information
@ -78,6 +90,19 @@ export const fileInfosFailure = createAction(
props<{ roomId: string; error: any }>()
);
export const fileDownCheck = createAction(
'[ucap::chat::chatting] fileDownCheck',
props<{ req: DownCheckRequest }>()
);
export const fileDownCheckSuccess = createAction(
'[ucap::chat::chatting] fileDownCheck Success',
props<{ res: DownCheckResponse }>()
);
export const fileDownCheckFailure = createAction(
'[ucap::chat::chatting] fileDownCheck Failure',
props<{ error: any }>()
);
/**
* add new event
*/
@ -143,3 +168,109 @@ export const sendNotification = createAction(
'[ucap::chat::chatting] Send Notification',
props<{ noti: SendNotification }>()
);
/** 대화 삭제 */
export const del = createAction(
'[ucap::chat::chatting] Delete',
props<DelRequest>()
);
export const delFailure = createAction(
'[ucap::chat::chatting] Delete Failure',
props<{ error: any }>()
);
export const delNotification = createAction(
'[ucap::chat::chatting] Delete Notification || Response',
props<{ noti: DelNotification | DelResponse }>()
);
/** 대화 삭제시 열린 대화방의 대화 내용 갱신 */
export const delEventList = createAction(
'[ucap::chat::chatting] Delete InfoList',
props<{
roomId: string;
eventSeqs: number[];
}>()
);
/** Clear event for TimerRoom */
export const intervalClearEvent = createAction(
'[ucap::chat::chatting] Clear events interval',
props<{
roomId: string;
}>()
);
/** forward */
export const forward = createAction(
'[ucap::chat::chatting] Forward',
props<{
senderSeq: string;
deviceType: DeviceType;
req: SendRequest;
trgtUserSeqs?: string[];
trgtRoomId?: string;
}>()
);
/** chat forward failure */
export const forwardFailure = createAction(
'[ucap::chat::chatting] Forward failure',
props<{ error: any }>()
);
export const forwardAfterRoomOpen = createAction(
'[ucap::chat::chatting] Forward after room open',
props<{
senderSeq: string;
deviceType: DeviceType;
req: SendRequest;
trgtUserSeqs?: string[];
trgtRoomId?: string;
}>()
);
export const roomOpenAfterForward = createAction(
'[ucap::chat::chatting] Room open after forward',
props<{
senderSeq: string;
req: SendRequest;
trgtUserSeqs?: string[];
trgtRoomId?: string;
}>()
);
export const forwarForFileEvent = createAction(
'[ucap::chat::chatting] Forward for file event',
props<{
forwardType: string;
deviceType: DeviceType;
trgtRoomId?: string;
sendReq: SendRequest;
}>()
);
/** 대화 회수 */
export const cancel = createAction(
'[ucap::chat::chatting] Cancel',
props<CancelRequest>()
);
export const cancelFailure = createAction(
'[ucap::chat::chatting] Cancel Failure',
props<{ error: any }>()
);
export const cancelNotification = createAction(
'[ucap::chat::chatting] Cancel Notification || Response',
props<{ noti: CancelNotification | CancelResponse }>()
);
/** 대화 회수시 열린 대화방의 대화 내용 갱신 */
export const updateEventList = createAction(
'[ucap::chat::chatting] Update InfoList',
props<{
roomId: string;
eventSeq: number;
sentMessage: string;
}>()
);
export const clearActiveRoomId = createAction(
'[ucap::chat::chatting] Clear activeRoomId',
props()
);

View File

@ -7,7 +7,9 @@ import {
exhaustMap,
concatMap,
withLatestFrom,
debounceTime
debounceTime,
mergeMap,
take
} from 'rxjs/operators';
import { Injectable, Inject } from '@angular/core';
@ -16,11 +18,46 @@ import { Store, select } from '@ngrx/store';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Dictionary } from '@ngrx/entity';
import {
RoomType,
OpenResponse as CreateResponse,
Open3Response as CreateTimerResponse,
ExitResponse as DeleteResponse,
ExitAllResponse as DeleteMultiResponse
} from '@ucap/protocol-room';
import {
InfoRequest,
ReadResponse,
FileType,
EventType,
DelResponse,
SendResponse,
CancelResponse,
FileEventJson,
decodeFileEventJson,
Info,
EventJson
} from '@ucap/protocol-event';
import moment from 'moment';
import { LocaleCode } from '@ucap/core';
import { StatusCode } from '@ucap/api';
import { FileTalkShareRequest, FileTalkShareResponse } from '@ucap/api-common';
import { I18nService } from '@ucap/ng-i18n';
import { CommonApiService } from '@ucap/ng-api-common';
import { EventProtocolService } from '@ucap/ng-protocol-event';
import { RoomProtocolService } from '@ucap/ng-protocol-room';
import { FileProtocolService } from '@ucap/ng-protocol-file';
import { UserSelector } from '@ucap/ng-store-organization';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { ModuleConfig } from '../../config/module-config';
import { _MODULE_CONFIG } from '../../config/token';
import { ChattingSelector, RoomSelector } from '../state';
import * as RoomActions from '../room/actions';
import {
events,
eventsFailure,
@ -35,18 +72,26 @@ import {
sendSuccess,
sendFailure,
addEvent,
addEventSuccess
addEventSuccess,
del,
delNotification,
delFailure,
delEventList,
moreEvents,
forward,
forwardFailure,
forwardAfterRoomOpen,
roomOpenAfterForward,
forwarForFileEvent,
cancel,
cancelFailure,
cancelNotification,
updateEventList,
intervalClearEvent,
fileDownCheck,
fileDownCheckSuccess,
fileDownCheckFailure
} from './actions';
import {
InfoRequest,
ReadResponse,
FileType,
EventType
} from '@ucap/protocol-event';
import { ModuleConfig } from '../../config/module-config';
import { _MODULE_CONFIG } from '../../config/token';
import { Chatting } from './state';
@Injectable()
@ -110,6 +155,40 @@ export class Effects {
{ dispatch: false }
);
moreEvents$ = createEffect(
() => {
return this.actions$.pipe(
ofType(moreEvents),
mergeMap(
(action) =>
of(action).pipe(
withLatestFrom(
this.store.pipe(
select(ChattingSelector.eventList, action.roomId)
)
)
),
(action, latestStoreData) => latestStoreData
),
tap(([req, eventList]) => {
if (!!eventList && eventList.length > 0) {
this.store.dispatch(
events({
req: {
roomId: req.roomId,
baseSeq: eventList.sort((a, b) => a.seq - b.seq)[0].seq,
requestCount:
this.moduleConfig?.eventRequestDefaultCount || 50
} as InfoRequest
})
);
}
})
);
},
{ dispatch: false }
);
read$ = createEffect(
() => {
return this.actions$.pipe(
@ -117,7 +196,14 @@ export class Effects {
exhaustMap((req) =>
this.eventProtocolService.read(req).pipe(
map((res: ReadResponse) => {
// room user lastReadEventSeq reset.
this.store.dispatch(readSuccess(res));
// room noReadCount reset.
this.store.dispatch(
RoomActions.updateUnreadCount({
roomId: res.roomId
})
);
}),
catchError((error) => of(readFailure({ error })))
)
@ -152,6 +238,23 @@ export class Effects {
{ dispatch: false }
);
fileDownCheck$ = createEffect(
() => {
return this.actions$.pipe(
ofType(fileDownCheck),
switchMap((action) => {
return this.fileProtocolService.downCheck(action.req).pipe(
map((res) => {
this.store.dispatch(fileDownCheckSuccess({ res }));
}),
catchError((error) => of(fileDownCheckFailure({ error })))
);
})
);
},
{ dispatch: false }
);
addEvent$ = createEffect(
() => {
return this.actions$.pipe(
@ -234,6 +337,352 @@ export class Effects {
{ dispatch: false }
);
del$ = createEffect(() =>
this.actions$.pipe(
ofType(del),
exhaustMap((req) =>
this.eventProtocolService.del(req).pipe(
map((res: DelResponse) => {
return delNotification({ noti: res });
}),
catchError((error) => of(delFailure({ error })))
)
)
)
);
delNotification$ = createEffect(
() => {
return this.actions$.pipe(
ofType(delNotification),
map((action) => action.noti),
withLatestFrom(this.store.pipe(select(RoomSelector.rooms))),
tap(([noti, rooms]) => {
const notiRoomId = noti.roomId;
if (!!rooms && rooms.length > 0) {
// 현재 방이 오픈되어 있으면 방내용 갱신
const idx = rooms.findIndex(
(roomInfo) => roomInfo.roomId === notiRoomId
);
if (idx > -1) {
this.store.dispatch(
delEventList({ roomId: notiRoomId, eventSeqs: [noti.eventSeq] })
);
}
}
// 대화 > 리스트의 항목 갱신
this.store.dispatch(
RoomActions.room({
req: {
roomId: notiRoomId,
isDetail: false,
localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode
}
})
);
})
);
},
{ dispatch: false }
);
intervalClearEvent$ = createEffect(
() => {
return this.actions$.pipe(
ofType(intervalClearEvent),
withLatestFrom(
this.store.pipe(select(RoomSelector.rooms)),
this.store.pipe(select(ChattingSelector.chattings))
),
tap(([action, rooms, chattings]) => {
const roomId = action.roomId;
if (
!!rooms &&
rooms.length > 0 &&
!!chattings &&
chattings.length > 0
) {
const roomInfo = rooms.find((item) => item.roomId === roomId);
const chatting = chattings.find((item) => item.roomId === roomId);
if (
!!roomInfo &&
!!roomInfo.timeRoomInterval &&
!!chatting &&
!!chatting.eventList
) {
const eventList = chatting.eventList;
const delEventSeq: number[] = [];
eventList.ids.forEach((id) => {
const event: Info<EventJson> = eventList.entities[id];
if (
!event ||
event.type === EventType.NotificationForTimerRoom
) {
//ignore..
} else {
if (
new Date().getTime() -
moment(event.sendDate).toDate().getTime() >=
roomInfo.timeRoomInterval * 1000
) {
delEventSeq.push(event.seq);
}
}
});
if (delEventSeq.length > 0) {
this.store.dispatch(
delEventList({
roomId,
eventSeqs: delEventSeq
})
);
}
}
}
})
);
},
{ dispatch: false }
);
forward$ = createEffect(
() => {
return this.actions$.pipe(
ofType(forward),
tap((action) => {
if (!!action.trgtRoomId) {
// 대화전달 후 방오픈. Exist roomSeq.
if (action.req.eventType === EventType.File) {
// file share request action
this.store.dispatch(
forwarForFileEvent({
forwardType: 'F',
deviceType: action.deviceType,
sendReq: action.req,
trgtRoomId: action.trgtRoomId
})
);
} else {
this.store.dispatch(roomOpenAfterForward(action));
}
} else if (!!action.trgtUserSeqs && action.trgtUserSeqs.length > 0) {
// 방오픈 후 대화전달.
this.store.dispatch(forwardAfterRoomOpen(action));
}
})
);
},
{ dispatch: false }
);
forwarForFileEvent$ = createEffect(
() => {
return this.actions$.pipe(
ofType(forwarForFileEvent),
withLatestFrom(
this.store.pipe(select(UserSelector.user)),
this.store.pipe(select(LoginSelector.loginRes))
),
tap(([actionReq, user, loginRes]) => {
const fileEventJson: FileEventJson = decodeFileEventJson(
actionReq.sendReq.sentMessage
);
const req: FileTalkShareRequest = {
userSeq: String(user.info.seq),
deviceType: actionReq.deviceType,
token: loginRes.tokenString,
attachmentsSeq: fileEventJson.attachmentSeq.toString(),
roomId: actionReq.trgtRoomId,
synapKey: ''
};
return this.commonApiService
.fileTalkShare(req)
.pipe(
take(1),
map((res: FileTalkShareResponse) => {
const sedRequest = {
senderSeq: String(user.info.seq),
req: {
roomId: actionReq.trgtRoomId,
eventType: actionReq.sendReq.eventType,
sentMessage: res.returnJson
}
};
if (actionReq.forwardType === 'O') {
if (res.statusCode === StatusCode.Success) {
this.store.dispatch(send(sedRequest));
}
} else if (actionReq.forwardType === 'F') {
if (res.statusCode === StatusCode.Success) {
this.store.dispatch(
roomOpenAfterForward({
...sedRequest,
trgtRoomId: actionReq.trgtRoomId
})
);
}
}
}),
catchError((error) => of(forwardFailure({ error })))
)
.subscribe();
})
);
},
{ dispatch: false }
);
forwardAfterRoomOpen$ = createEffect(
() => {
let createRes: CreateResponse;
return this.actions$.pipe(
ofType(forwardAfterRoomOpen),
exhaustMap((actionReq) => {
return this.roomProtocolService
.open({ divCd: 'forwardOpen', userSeqs: actionReq.trgtUserSeqs })
.pipe(
map((res: CreateResponse) => {
createRes = res;
this.store.dispatch(
RoomActions.createRoomSuccess({ res, isForward: true })
);
}),
map(() => {
if (actionReq.req.eventType === EventType.File) {
// file share request action
this.store.dispatch(
forwarForFileEvent({
forwardType: 'O',
deviceType: actionReq.deviceType,
sendReq: actionReq.req,
trgtRoomId: createRes.roomId
})
);
} else {
this.store.dispatch(
send({
senderSeq: actionReq.senderSeq,
req: {
roomId: createRes.roomId,
eventType: actionReq.req.eventType,
sentMessage: actionReq.req.sentMessage
}
})
);
}
}),
catchError((error) =>
of(RoomActions.createRoomFailure({ error }))
)
);
})
);
},
{ dispatch: false }
);
roomOpenAfterForward$ = createEffect(
() => {
return this.actions$.pipe(
ofType(roomOpenAfterForward),
exhaustMap((action) => {
return this.eventProtocolService
.send({
roomId: action.trgtRoomId,
eventType: action.req.eventType,
sentMessage: action.req.sentMessage
})
.pipe(
map((res: SendResponse) => {
this.store.dispatch(
addEvent({
roomId: res.roomId,
info: res.info,
SVC_TYPE: res.SVC_TYPE,
SSVC_TYPE: res.SSVC_TYPE
})
);
this.store.dispatch(
RoomActions.selectedRoom({
roomId: res.roomId,
localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode
})
);
}),
catchError((error) =>
of(RoomActions.createRoomFailure({ error }))
)
);
})
);
},
{ dispatch: false }
);
cancel$ = createEffect(() =>
this.actions$.pipe(
ofType(cancel),
exhaustMap((req) =>
this.eventProtocolService.cancel(req).pipe(
map((res: CancelResponse) => {
return cancelNotification({ noti: res });
}),
catchError((error) => of(delFailure({ error })))
)
)
)
);
cancelNotification$ = createEffect(
() => {
return this.actions$.pipe(
ofType(cancelNotification),
withLatestFrom(this.store.pipe(select(RoomSelector.rooms))),
tap(([action, rooms]) => {
const notiRoomId = action.noti.roomId;
if (!!rooms && rooms.length > 0) {
// 현재 방이 오픈되어 있으면 방내용 갱신
const idx = rooms.findIndex(
(roomInfo) => roomInfo.roomId === notiRoomId
);
if (idx > -1) {
this.store.dispatch(
updateEventList({
roomId: notiRoomId,
eventSeq: action.noti.eventSeq,
sentMessage: this.i18nService.t('event.recalled')
})
);
}
}
// 대화 > 리스트의 항목 갱신
this.store.dispatch(
RoomActions.room({
req: {
roomId: action.noti.roomId,
isDetail: false,
localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode
}
})
);
})
);
},
{ dispatch: false }
);
/*******************************************************************
* [Room Action watching.]
*******************************************************************/
@ -263,6 +712,10 @@ export class Effects {
@Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig,
private roomProtocolService: RoomProtocolService,
private eventProtocolService: EventProtocolService,
private fileProtocolService: FileProtocolService
) {}
private fileProtocolService: FileProtocolService,
private i18nService: I18nService,
private commonApiService: CommonApiService
) {
this.i18nService.setDefaultNamespace('chat');
}
}

View File

@ -1,21 +1,28 @@
import { createReducer, on } from '@ngrx/store';
import { FileInfo } from '@ucap/protocol-file';
import { Info, EventJson, EventType } from '@ucap/protocol-event';
import * as RoomActions from '../room/actions';
import {
initialState,
adapterChatting,
adapterEventList,
Chatting,
adapterFileInfoList,
adapterFileInfoCheckList
adapterFileInfoList
// adapterFileInfoCheckList
} from './state';
import * as RoomActions from '../room/actions';
import {
eventsSuccess,
eventsFailure,
fileInfosSuccess,
fileInfosFailure,
addEvent
addEvent,
delEventList,
updateEventList,
clearActiveRoomId
} from './actions';
export const reducer = createReducer(
@ -31,22 +38,53 @@ export const reducer = createReducer(
eventListProcessing: false,
eventList: adapterEventList.getInitialState(),
eventStatus: null,
remainEvent: false,
remainEvent: null,
fileInfoListProcessing: false,
fileInfoList: adapterFileInfoList.getInitialState(),
fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
// fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
fileInfoCheckList: [],
fileInfoSyncDate: '',
...chatting
};
// dupliaction event process
const trgtEventInfoList: Info<EventJson>[] = [];
if (!!chatting && !!chatting.eventList && !!chatting.eventList.entities) {
const filteredList = action.eventInfoList.filter((item) => {
let notExistOrDiff = true; // added target flag.
// tslint:disable-next-line: forin
for (const key in chatting.eventList.entities) {
const event = chatting.eventList.entities[key];
if (
item.seq === event.seq &&
item.type === event.type &&
item.sentMessage === event.sentMessage
) {
notExistOrDiff = false;
break;
}
}
return notExistOrDiff;
});
if (!!filteredList && filteredList.length > 0) {
trgtEventInfoList.push(...filteredList);
}
} else {
trgtEventInfoList.push(...action.eventInfoList);
}
trgtChatting = {
...trgtChatting,
eventList: adapterEventList.upsertMany(action.eventInfoList, {
eventList: adapterEventList.upsertMany(trgtEventInfoList, {
...trgtChatting.eventList
}),
eventStatus: action.res,
remainEvent: action.remainEvent,
remainEvent:
trgtChatting.remainEvent === false ? false : action.remainEvent, // 재조회를 위한 처리.
eventListProcessing: false
};
@ -92,11 +130,12 @@ export const reducer = createReducer(
eventListProcessing: false,
eventList: adapterEventList.getInitialState(),
eventStatus: null,
remainEvent: false,
remainEvent: null,
fileInfoListProcessing: false,
fileInfoList: adapterFileInfoList.getInitialState(),
fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
// fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
fileInfoCheckList: [],
fileInfoSyncDate: '',
...chatting
};
@ -111,11 +150,12 @@ export const reducer = createReducer(
...trgtChatting.fileInfoList
})
: trgtChatting.fileInfoList,
fileInfoCheckList: !!fileInfoCheckList
? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, {
...trgtChatting.fileInfoCheckList
})
: trgtChatting.fileInfoCheckList,
// fileInfoCheckList: !!fileInfoCheckList
// ? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, {
// ...trgtChatting.fileInfoCheckList
// })
// : trgtChatting.fileInfoCheckList,
fileInfoCheckList,
fileInfoListProcessing: false
};
@ -171,6 +211,117 @@ export const reducer = createReducer(
}
}),
on(delEventList, (state, action) => {
const roomId = action.roomId;
const chatting = state.chattings.entities[roomId];
// checked Valid array
if (
!chatting ||
!chatting.eventList ||
chatting.eventList.ids.length === 0
) {
return state;
}
const fileInfoList = chatting.fileInfoList;
const trgtDelFileInfoSeqs = [];
if (!!fileInfoList) {
fileInfoList.ids.forEach((id) => {
const fileInfo: FileInfo = fileInfoList.entities[id];
if (action.eventSeqs.indexOf(fileInfo.eventSeq) > -1) {
trgtDelFileInfoSeqs.push(id);
}
});
}
return {
...state,
chattings: adapterChatting.upsertOne(
{
...chatting,
eventList: adapterEventList.removeMany(action.eventSeqs, {
...chatting.eventList
}),
fileInfoList:
trgtDelFileInfoSeqs.length > 0
? adapterFileInfoList.removeMany(trgtDelFileInfoSeqs, {
...chatting.fileInfoList
})
: chatting.fileInfoList
},
{ ...state.chattings }
)
};
}),
on(updateEventList, (state, action) => {
const roomId = action.roomId;
const eventSeq = action.eventSeq;
const sentMessage = action.sentMessage;
const chatting = state.chattings.entities[roomId];
let fileInfoSeq;
// checked Valid array
if (
!chatting ||
!chatting.eventList ||
chatting.eventList.ids.length === 0
) {
return state;
}
const statusEventInfo: Info<EventJson> = {
...chatting.eventList[eventSeq],
type: EventType.RecalledMessage,
sentMessage
};
// 파일이 회수되었을 경우 fileInfoList 에서도 삭제 한다.
if (chatting.eventList.entities[eventSeq].type === EventType.File) {
const fileInfoList = chatting.fileInfoList;
if (!!chatting && !!fileInfoList) {
fileInfoList.ids.forEach((id) => {
const fileInfo: FileInfo = fileInfoList.entities[id];
if (action.eventSeq === fileInfo.eventSeq) {
fileInfoSeq = id;
}
});
}
}
return {
...state,
chattings: adapterChatting.upsertOne(
{
...chatting,
eventList: adapterEventList.updateOne(
{
id: eventSeq,
changes: statusEventInfo
},
{
...chatting.eventList
}
),
fileInfoList: !!fileInfoSeq
? adapterFileInfoList.removeOne(fileInfoSeq, {
...chatting.fileInfoList
})
: chatting.fileInfoList
},
{ ...state.chattings }
)
};
}),
on(clearActiveRoomId, (state, action) => {
return {
...state,
activeRoomId: null
};
}),
/*******************************************************************
* [Room Action watching.]
*******************************************************************/
@ -197,7 +348,9 @@ export const reducer = createReducer(
return {
...state,
chattings: adapterChatting.removeMany(roomIds, { ...state.chattings })
chattings: adapterChatting.removeMany(roomIds, {
...state.chattings
})
};
})
);

View File

@ -2,12 +2,11 @@ import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity';
import { InfoResponse, Info, EventJson } from '@ucap/protocol-event';
import { FileInfo, FileDownloadInfo } from '@ucap/protocol-file';
export interface EventListState extends EntityState<Info<EventJson>> {}
export interface FileInfoListState extends EntityState<FileInfo> {}
export interface FileInfoCheckListState extends EntityState<FileDownloadInfo> {}
// export interface FileInfoCheckListState extends EntityState<FileDownloadInfo> {}
export const adapterEventList = createEntityAdapter<Info<EventJson>>({
selectId: (info) => info.seq,
@ -21,12 +20,12 @@ export const adapterFileInfoList = createEntityAdapter<FileInfo>({
return b.seq - a.seq;
}
});
export const adapterFileInfoCheckList = createEntityAdapter<FileDownloadInfo>({
selectId: (info) => info.seq,
sortComparer: (a, b) => {
return b.seq - a.seq;
}
});
// export const adapterFileInfoCheckList = createEntityAdapter<FileDownloadInfo>({
// selectId: (info) => info.seq,
// sortComparer: (a, b) => {
// return b.seq - a.seq;
// }
// });
const eventListInitialState: EventListState = adapterEventList.getInitialState(
{}
@ -34,9 +33,9 @@ const eventListInitialState: EventListState = adapterEventList.getInitialState(
const fileInfoListInitialState: FileInfoListState = adapterFileInfoList.getInitialState(
{}
);
const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState(
{}
);
// const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState(
// {}
// );
const {
selectAll: selectAllForEventList,
@ -52,12 +51,12 @@ const {
selectTotal: selectTotalForFileInfoList
} = adapterFileInfoList.getSelectors();
const {
selectAll: selectAllForFileInfoCheckList,
selectEntities: selectEntitiesForFileInfoCheckList,
selectIds: selectIdsForFileInfoCheckList,
selectTotal: selectTotalForFileInfoCheckList
} = adapterFileInfoCheckList.getSelectors();
// const {
// selectAll: selectAllForFileInfoCheckList,
// selectEntities: selectEntitiesForFileInfoCheckList,
// selectIds: selectIdsForFileInfoCheckList,
// selectTotal: selectTotalForFileInfoCheckList
// } = adapterFileInfoCheckList.getSelectors();
export interface Chatting {
roomId?: string;
@ -65,11 +64,12 @@ export interface Chatting {
eventListProcessing?: boolean;
eventList?: EventListState;
eventStatus?: InfoResponse | null;
remainEvent?: boolean;
remainEvent?: boolean | null;
fileInfoListProcessing?: boolean;
fileInfoList?: FileInfoListState;
fileInfoCheckList?: FileInfoCheckListState;
// fileInfoCheckList?: FileInfoCheckListState;
fileInfoCheckList?: FileDownloadInfo[];
fileInfoSyncDate?: string;
}
@ -139,12 +139,37 @@ export function selectors<S>(selector: Selector<any, State>) {
(state) => state.fileInfoListProcessing
);
const selectChattingFileInfoList = createSelector(
selectChatting,
(state) => state.fileInfoList
selectChattings,
(state: ChattingState, roomId: string) => {
const chatting = state.entities && state.entities[roomId];
if (!!chatting) {
return chatting?.fileInfoList;
} else {
return adapterFileInfoList.getInitialState();
}
}
);
// const selectChattingFileInfoCheckList = createSelector(
// selectChattings,
// (state: ChattingState, roomId: string) => {
// const chatting = state.entities && state.entities[roomId];
// if (!!chatting) {
// return chatting?.fileInfoCheckList;
// } else {
// return adapterFileInfoCheckList.getInitialState();
// }
// }
// );
const selectChattingFileInfoCheckList = createSelector(
selectChatting,
(state) => state.fileInfoCheckList
selectChattings,
(state: ChattingState, roomId: string) => {
const chatting = state.entities && state.entities[roomId];
if (!!chatting) {
return chatting?.fileInfoCheckList;
} else {
return [];
}
}
);
const selectChattingFileInfoSyncDate = createSelector(
selectChatting,
@ -167,10 +192,11 @@ export function selectors<S>(selector: Selector<any, State>) {
selectChattingFileInfoList,
selectAllForFileInfoList
),
fileInfoCheckList: createSelector(
selectChattingFileInfoCheckList,
selectAllForFileInfoCheckList
),
// fileInfoCheckList: createSelector(
// selectChattingFileInfoCheckList,
// selectAllForFileInfoCheckList
// ),
fileInfoCheckList: selectChattingFileInfoCheckList,
fileInfoSyncDate: selectChattingFileInfoSyncDate
};
}

View File

@ -160,21 +160,6 @@ export const create = createAction(
'[ucap::chat::room] create',
props<{ req: CreateRequest }>()
);
/**
* Success of create request
*/
export const createSuccess = createAction(
'[ucap::chat::room] create Success',
props<{ res: CreateResponse }>()
);
/**
* Failure of create request
*/
export const createFailure = createAction(
'[ucap::chat::room] create Failure',
props<{ error: any }>()
);
/**
* create timer room
*/
@ -183,17 +168,17 @@ export const createTimer = createAction(
props<{ req: CreateTimerRequest }>()
);
/**
* Success of openTimer request
* Success of create room / timer room request
*/
export const createTimerSuccess = createAction(
'[ucap::chat::room] createTimer Success',
props<{ res: CreateTimerResponse }>()
export const createRoomSuccess = createAction(
'[ucap::chat::room] create room Success',
props<{ res: CreateResponse; isForward?: boolean }>()
);
/**
* Failure of createTimer request
* Failure of create room / timer room request
*/
export const createTimerFailure = createAction(
'[ucap::chat::room] createTimer Failure',
export const createRoomFailure = createAction(
'[ucap::chat::room] create room Failure',
props<{ error: any }>()
);
@ -264,6 +249,15 @@ export const updateFailure = createAction(
'[ucap::chat::room] update Failure',
props<{ error: any }>()
);
/**
* Success of update RoomName
*/
export const updateRoomName = createAction(
'[ucap::chat::room] update Roomname for Notification',
props<{
res: UpdateResponse;
}>()
);
/**
* update isJoinRoom of user information from true to false
@ -331,18 +325,6 @@ export const closeFailure = createAction(
props<{ error: any }>()
);
/**
* Invite conversation partner to room or create room
*/
export const inviteOrCreate = createAction(
'[ucap::chat::room] inviteOrCreate',
props<{
roomInfo: RoomInfo;
localeCode: LocaleCode;
req: CreateRequest;
}>()
);
/**
* Invite conversation partner to room
*/
@ -386,6 +368,13 @@ export const expelFailure = createAction(
'[ucap::chat::room] expel Failure',
props<{ error: any }>()
);
/**
* expel notification
*/
export const expelNotification = createAction(
'[ucap::chat::room] expel Notification',
props<{ res: ExitForcingResponse }>()
);
/**
* update interval of timer room
@ -422,10 +411,24 @@ export const inviteNotification = createAction(
*/
export const exitNotification = createAction(
'[ucap::chat::room] Exit Notification',
props<{ roomId: string; userSeq: string; senderSeq: string }>()
props<{ roomId: string; senderSeq: string }>()
);
export const updateUnreadCount = createAction(
'[ucap::chat::room] Update unread count',
props<{ roomId: string; noReadCnt?: number }>()
);
/**
* room user info update by Notification
*/
// export const userInfoListUpdate = createAction(
// '[ucap::chat::room] user info update',
// props<{
// roomInfo: RoomInfo;
// roomUserInfo: {
// userInfoShortList: UserInfoShort[];
// userInfoList: RoomUserInfo[];
// };
// }>()
// );

View File

@ -14,8 +14,8 @@ import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { LocaleCode } from '@ucap/core';
import {
RoomType,
OpenResponse as CreateResponse,
Open3Response as CreateTimerResponse,
ExitResponse as DeleteResponse,
@ -24,22 +24,30 @@ import {
InviteResponse,
ExitForcingResponse,
UpdateTimerSetResponse,
InfoRequest
InfoRequest,
UserInfoShort,
UserInfo,
RoomInfo
} from '@ucap/protocol-room';
import { UserNotification, UserInfoUpdateType } from '@ucap/protocol-info';
import { I18nService } from '@ucap/ng-i18n';
import { RoomProtocolService } from '@ucap/ng-protocol-room';
import { SyncProtocolService } from '@ucap/ng-protocol-sync';
import {
PresenceActions,
CommonActions,
UserSelector
} from '@ucap/ng-store-organization';
import { LoginActions } from '@ucap/ng-store-authentication';
import * as ChattingAction from '../Chatting/actions';
import { RoomSelector } from '../state';
import * as ChattingAction from '../chatting/actions';
import { RoomSelector, ChattingSelector } from '../state';
import {
rooms,
roomsFailure,
roomsSuccess,
room,
roomSuccess,
roomFailure,
inviteNotification,
exitNotification,
@ -48,11 +56,9 @@ import {
close,
delSuccess,
create,
createSuccess,
createFailure,
createTimer,
createTimerSuccess,
createTimerFailure,
createRoomSuccess,
createRoomFailure,
del,
delFailure,
update,
@ -61,7 +67,6 @@ import {
open,
openSuccess,
closeSuccess,
inviteOrCreate,
invite,
inviteSuccess,
inviteFailure,
@ -79,10 +84,9 @@ import {
selectedRoom,
room2Success,
selectedRoomSuccess,
clearSelectedRoom
clearSelectedRoom,
expelNotification
} from './actions';
import { Router } from '@angular/router';
import { LocaleCode } from '@ucap/core';
@Injectable()
export class Effects {
@ -121,14 +125,25 @@ export class Effects {
roomInfo2Res: res
})
);
// Buddy Presence
const targetUserInfos = req.isDetail
? res.roomUserInfo.userInfoList.map(
(userInfo) => userInfo.seq + ''
)
: res.roomUserInfo.userInfoShortList.map(
(userInfo) => userInfo.seq + ''
);
if (!!targetUserInfos && targetUserInfos.length > 0) {
this.store.dispatch(
PresenceActions.bulkInfo({
divCd: 'roomBulk',
userSeqs: targetUserInfos
})
);
}
} else {
// is not join room. so, redirect chat main.
this.router.navigate([
'chat',
{
outlets: { content: 'index' }
}
]);
this.store.dispatch(
clearSelectedRoom({ roomId: req.roomId })
);
@ -237,27 +252,9 @@ export class Effects {
exhaustMap((req) => {
return this.roomProtocolService.open(req).pipe(
map((res: CreateResponse) => {
console.log('CreateResponse', res);
this.store.dispatch(createSuccess({ res }));
this.router.navigate(
[
'chat',
{
outlets: { content: 'chatroom' }
}
],
{
queryParams: { roomId: res.roomId }
}
);
// if (!res.newRoom) {
// } else {
// }
this.store.dispatch(createRoomSuccess({ res }));
}),
catchError((error) => of(createFailure({ error })))
catchError((error) => of(createRoomFailure({ error })))
);
})
);
@ -265,33 +262,53 @@ export class Effects {
{ dispatch: false }
);
createTimer$ = createEffect(() =>
this.actions$.pipe(
createTimer$ = createEffect(
() => {
return this.actions$.pipe(
ofType(createTimer),
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.open3(req).pipe(
map((res: CreateTimerResponse) => {
return createTimerSuccess({ res });
// return createTimerSuccess({ res });
this.store.dispatch(createRoomSuccess({ res }));
}),
catchError((error) => of(createTimerFailure({ error })))
catchError((error) => of(createRoomFailure({ error })))
);
})
)
);
},
{ dispatch: false }
);
del$ = createEffect(() =>
this.actions$.pipe(
ofType(del),
map((action) => action.req),
exhaustMap((req) => {
withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))),
exhaustMap(([req, activeRoomId]) => {
const existActiveRoomId = req.roomId === activeRoomId;
return this.roomProtocolService.exit(req).pipe(
switchMap((res: DeleteResponse) => [
switchMap((res: DeleteResponse) => {
if (!!existActiveRoomId) {
return [
// clear activeRoomId
clearSelectedRoom({ roomId: res.roomId }),
// close room, clear chatting
close({ roomIds: [res.roomId] }),
// clear room in rooms.
delSuccess({ res })
]),
];
} else {
return [
// close room, clear chatting
close({ roomIds: [res.roomId] }),
// clear room in rooms.
delSuccess({ res })
];
}
}),
catchError((error) => of(delFailure({ error })))
);
})
@ -302,14 +319,32 @@ export class Effects {
this.actions$.pipe(
ofType(delMulti),
map((action) => action.req),
exhaustMap((req) => {
withLatestFrom(this.store.pipe(select(ChattingSelector.activeRoomId))),
exhaustMap(([req, activeRoomId]) => {
const existActiveRoomId = req.roomIds.find(
(roomId) => roomId === activeRoomId
);
return this.roomProtocolService.exitAll(req).pipe(
switchMap((res: DeleteMultiResponse) => [
switchMap((res: DeleteMultiResponse) => {
if (!!existActiveRoomId) {
return [
// clear selected room
clearSelectedRoom({ roomId: existActiveRoomId }),
// close room, clear chatting
close({ roomIds: res.roomIds }),
// clear room in rooms.
delMultiSuccess({ res })
]),
];
} else {
return [
// close room, clear chatting
close({ roomIds: res.roomIds }),
// clear room in rooms.
delMultiSuccess({ res })
];
}
}),
catchError((error) => of(delMultiFailure({ error })))
);
})
@ -357,33 +392,6 @@ export class Effects {
);
});
inviteOrCreate$ = createEffect(() =>
this.actions$.pipe(
ofType(inviteOrCreate),
map((action) => {
const roomInfo = action.roomInfo;
const localeCode = action.localeCode;
switch (roomInfo.roomType) {
case RoomType.Single:
return create({ req: action.req });
case RoomType.Multi:
return invite({
req: {
roomId: roomInfo.roomId,
inviteUserSeqs: [...action.req.userSeqs]
},
localeCode
});
default:
return inviteFailure({
error: `type[${roomInfo.roomType}] of room is not valid`
});
}
})
)
);
invite$ = createEffect(() =>
this.actions$.pipe(
ofType(invite),
@ -398,7 +406,7 @@ export class Effects {
room({
req: {
roomId: req.roomId,
isDetail: true,
isDetail: false,
localeCode
}
})
@ -425,6 +433,47 @@ export class Effects {
)
);
expelSuccess$ = createEffect(
() => {
return this.actions$.pipe(
ofType(expelSuccess),
map((action) => action.res),
tap((res) => {
this.store.dispatch(
excludeUser({ roomId: res.roomId, userSeqs: res.userSeqs })
);
})
);
},
{ dispatch: false }
);
expelNotification$ = createEffect(() => {
return this.actions$.pipe(
ofType(expelNotification),
withLatestFrom(this.store.pipe(select(UserSelector.user))),
exhaustMap(([action, user]) => {
const roomId = action.res.roomId;
const userSeqs = action.res.userSeqs;
const existMe = userSeqs.indexOf(String(user.info.seq)) > -1;
// 내가 강퇴 대상에 포함되어 있으면 우선 처리.
if (!!existMe) {
return [
close({ roomIds: [roomId] }),
clearSelectedRoom({ roomId }),
delSuccess({ res: { roomId } })
];
} else {
// 나를 제외한 강퇴 인원 처리.
if (!!userSeqs && userSeqs.length > 0) {
return [excludeUser({ roomId, userSeqs })];
}
}
})
);
});
updateTimeRoomInterval$ = createEffect(() =>
this.actions$.pipe(
ofType(updateTimeRoomInterval),
@ -440,28 +489,48 @@ export class Effects {
)
);
inviteNotification$ = createEffect(() => {
/**
* @discription Call by notifications case in SSVC_TYPE_ROOM_INVITE_RES, SSVC_TYPE_ROOM_INVITE_NOTI
* 1. roomlist .( .)
* 2. roomlist .
*/
inviteNotification$ = createEffect(
() => {
return this.actions$.pipe(
ofType(inviteNotification),
map((action) =>
withLatestFrom(this.store.pipe(select(RoomSelector.rooms))),
map(([action, roomList]) => {
const roomId = action.noti.roomId;
if (!!roomList && roomList.length > 0) {
if (roomList.some((roomInfo) => roomId === roomInfo.roomId)) {
this.store.dispatch(
room({
req: {
roomId: action.noti.roomId,
isDetail: true,
roomId,
isDetail: false,
localeCode: action.localeCode
}
})
)
);
});
}
}
})
);
},
{
dispatch: false
}
);
exitNotification$ = createEffect(() => {
return this.actions$.pipe(
ofType(exitNotification),
switchMap((action) => {
if (action.userSeq === action.senderSeq) {
withLatestFrom(this.store.pipe(select(UserSelector.user))),
switchMap(([action, user]) => {
if (String(user.info.seq) === String(action.senderSeq)) {
return [
close({ roomIds: [action.roomId] }),
clearSelectedRoom({ roomId: action.roomId }),
delSuccess({
res: { roomId: action.roomId }
})
@ -490,7 +559,134 @@ export class Effects {
const roomId = action.roomId;
if (!roomList.find((roomInfo) => roomInfo.roomId === roomId)) {
this.store.dispatch(rooms({ localeCode: LocaleCode.Korean }));
this.store.dispatch(
rooms({
localeCode: this.i18nService.currentLng.toUpperCase() as LocaleCode
})
);
}
})
);
},
{ dispatch: false }
);
userNotificationForRoom$ = createEffect(
() => {
return this.actions$.pipe(
ofType(CommonActions.userNotification),
withLatestFrom(
this.store.pipe(select(UserSelector.user)),
this.store.pipe(select(RoomSelector.rooms)),
this.store.pipe(select(RoomSelector.roomUsers)),
this.store.pipe(select(RoomSelector.roomUsersShort))
),
tap(([action, user, roomList, roomUsers, roomUsersShort]) => {
const noti = action.noti as UserNotification;
if (
Number(action.noti.SENDER_SEQ) !== Number(user.info.seq) &&
noti.type === UserInfoUpdateType.Image
) {
roomUsers = (roomUsers || []).filter(
(userMap) =>
roomList.findIndex((rInfo) => rInfo.roomId === userMap.roomId) >
-1
);
roomUsersShort = (roomUsersShort || []).filter(
(userMap) =>
roomList.findIndex((rInfo) => rInfo.roomId === userMap.roomId) >
-1
);
const tempRoomList: {
roomInfo: RoomInfo;
uInfos?: UserInfo[];
userInfoS?: UserInfoShort[];
}[] = [];
const findIdx = noti.info.indexOf('ProfileImage');
let imgInfo: string = noti.info;
if (findIdx > -1) {
const startIdx = noti.info.indexOf('/', findIdx);
imgInfo = noti.info.substring(startIdx);
}
for (const ru of roomUsers) {
ru.userInfos.every((u) => {
if (Number(u.seq) === Number(noti.SENDER_SEQ)) {
const tempInfos: UserInfo[] = [];
const roomInfo = roomList.filter(
(r) => r.roomId === ru.roomId
)[0];
const cUser = {
...u,
profileImageFile: imgInfo
};
ru.userInfos.map((u2) => {
if (Number(u2.seq) === Number(u.seq)) {
tempInfos.push(cUser);
} else {
tempInfos.push(u2);
}
});
tempRoomList.push({
roomInfo,
uInfos: tempInfos
});
return false;
}
return true;
});
}
for (const ru of roomUsersShort) {
ru.userInfos.every((u) => {
if (Number(u.seq) === Number(noti.SENDER_SEQ)) {
const tempShorts: UserInfoShort[] = [];
const roomInfo = roomList.filter(
(r) => r.roomId === ru.roomId
)[0];
const cUser = {
...u,
profileImageFile: imgInfo
};
ru.userInfos.map((u2) => {
if (Number(u2.seq) === Number(u.seq)) {
tempShorts.push(cUser);
} else {
tempShorts.push(u2);
}
});
tempRoomList.push({
roomInfo,
userInfoS: tempShorts
});
return false;
}
return true;
});
}
tempRoomList.map((obj) => {
this.store.dispatch(
room2Success({
roomInfo: obj.roomInfo,
roomUserInfo: {
userInfoList: obj?.uInfos,
userInfoShortList: obj?.userInfoS
}
})
);
});
}
})
);
@ -501,8 +697,8 @@ export class Effects {
constructor(
private actions$: Actions,
private store: Store<any>,
private router: Router,
private syncProtocolService: SyncProtocolService,
private roomProtocolService: RoomProtocolService
private roomProtocolService: RoomProtocolService,
private i18nService: I18nService
) {}
}

View File

@ -1,12 +1,10 @@
import { createReducer, on } from '@ngrx/store';
import {
UserInfo as RoomUserInfo,
UserInfoShort as RoomUserInfoShort,
RoomInfo
} from '@ucap/protocol-room';
import { RoomInfo } from '@ucap/protocol-room';
import { Info, EventJson } from '@ucap/protocol-event';
import * as chattingActions from '../chatting/actions';
import * as ChattingActions from '../chatting/actions';
import { ChatUtil } from '../../utils/chat.util';
import {
initialState,
@ -19,18 +17,17 @@ import {
import {
roomsSuccess,
roomSuccess,
excludeUser,
excludeUserSuccess,
delSuccess,
rooms2Success,
delMultiSuccess,
updateSuccess,
room2Success,
createSuccess,
updateUnreadCount
createRoomSuccess,
updateUnreadCount,
updateRoomName,
updateTimeRoomIntervalSuccess
} from './actions';
import { Info, EventJson } from '@ucap/protocol-event';
import { ChatUtil } from '../../utils/chat.util';
export const reducer = createReducer(
initialState,
@ -184,63 +181,88 @@ export const reducer = createReducer(
on(excludeUserSuccess, (state, action) => {
const roomId = action.roomId;
const roomUserMap = state.roomUsers.entities[roomId];
const roomUserMapShort = state.roomUsersShort.entities[roomId];
const userInfos: RoomUserInfo[] = [...roomUserMap.userInfos];
const userInfosShort: RoomUserInfoShort[] = [...roomUserMapShort.userInfos];
action.userSeqs.forEach((userSeq) => {
const userInfo: RoomUserInfo = userInfos.find(
(u) => userSeq === String(u.seq)
);
if (!!userInfo && !!userInfo.seq) {
userInfo.isJoinRoom = false;
}
const userInfoShort: RoomUserInfoShort = userInfosShort.find(
(u) => userSeq === String(u.seq)
);
if (!!userInfoShort && !!userInfoShort.seq) {
userInfoShort.isJoinRoom = false;
}
});
let roomUser: RoomUserMap = state.roomUsers.entities[roomId];
if (!!roomUser) {
roomUser = {
...roomUser,
userInfos: state.roomUsers.entities[roomId].userInfos.map(
(roomUserInfo) => {
if (
action.userSeqs.some((seq) => seq + '' === roomUserInfo.seq + '')
) {
return {
...state,
roomUsers: adapterRoomUser.updateOne(
{
id: roomId,
changes: {
roomId,
userInfos
...roomUserInfo,
isJoinRoom: false
};
} else {
return roomUserInfo;
}
},
{
...state.roomUsers
}
),
roomUsersShort: adapterRoomUserShort.updateOne(
{
id: roomId,
changes: {
roomId,
userInfos: userInfosShort
}
},
{
...state.roomUsersShort
}
)
};
}
let roomUserShort: RoomUserShortMap = state.roomUsersShort.entities[roomId];
if (!!roomUserShort) {
roomUserShort = {
...roomUserShort,
userInfos: state.roomUsersShort.entities[roomId].userInfos.map(
(roomUserInfo) => {
if (
action.userSeqs.some((seq) => seq + '' === roomUserInfo.seq + '')
) {
return {
...roomUserInfo,
isJoinRoom: false
};
} else {
return roomUserInfo;
}
}
)
};
}
const currentRoomInfo = state.rooms.entities[roomId];
let roomInfo: RoomInfo;
if (!!currentRoomInfo) {
roomInfo = {
...currentRoomInfo,
joinUserCount: !!roomUser
? roomUser.userInfos.filter((item) => !!item.isJoinRoom).length
: !!roomUserShort
? roomUserShort.userInfos.filter((item) => !!item.isJoinRoom).length
: currentRoomInfo.joinUserCount
};
}
return {
...state,
rooms: !!roomInfo
? adapterRoom.upsertOne(roomInfo, { ...state.rooms })
: state.rooms,
roomUsers: !!roomUser
? adapterRoomUser.upsertOne(roomUser, {
...state.roomUsers
})
: state.roomUsers,
roomUsersShort: !!roomUserShort
? adapterRoomUserShort.upsertOne(roomUserShort, {
...state.roomUsersShort
})
: state.roomUsersShort
};
}),
on(createSuccess, (state, action) => {
on(createRoomSuccess, (state, action) => {
const standby = state.standbyRooms;
const curRoomId = action.res.roomId;
if (standby.findIndex((roomId) => roomId === curRoomId) > -1) {
if (
!!action.isForward ||
standby.findIndex((roomId) => roomId === curRoomId) > -1 ||
!!state.rooms.entities[curRoomId]
) {
// Exist.
return state;
} else {
@ -252,26 +274,6 @@ export const reducer = createReducer(
}
}),
on(delSuccess, (state, action) => {
const roomId = action.res.roomId;
const room = state.rooms.entities[roomId];
if (!room) {
return state;
}
return {
...state,
rooms: adapterRoom.removeOne(roomId, { ...state.rooms }),
roomUsers: adapterRoomUser.removeOne(roomId, {
...state.roomUsers
}),
roomUsersShort: adapterRoomUserShort.removeOne(roomId, {
...state.roomUsersShort
})
};
}),
on(updateSuccess, (state, action) => {
const roomInfo = {
...state.rooms.entities[action.res.roomId],
@ -285,6 +287,30 @@ export const reducer = createReducer(
};
}),
on(updateTimeRoomIntervalSuccess, (state, action) => {
const roomInfo = {
...state.rooms.entities[action.res.roomId],
timeRoomInterval: action.res.timerInterval
} as RoomInfo;
return {
...state,
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms })
};
}),
on(updateRoomName, (state, action) => {
const roomInfo = {
...state.rooms.entities[action.res.roomId],
roomName: action.res.roomName
} as RoomInfo;
return {
...state,
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms })
};
}),
on(delSuccess, (state, action) => {
const roomId = action.res.roomId;
const room = state.rooms.entities[roomId];
@ -353,7 +379,7 @@ export const reducer = createReducer(
/*******************************************************************
* [Chatting Action watching.]
*******************************************************************/
on(chattingActions.readSuccess, (state, action) => {
on(ChattingActions.readSuccess, (state, action) => {
const roomId = action.roomId;
const trgtUserSeq = action.SENDER_SEQ;
@ -413,7 +439,7 @@ export const reducer = createReducer(
};
}),
on(chattingActions.addEventSuccess, (state, action) => {
on(ChattingActions.addEventSuccess, (state, action) => {
const roomId = action.roomId;
const info: Info<EventJson> = action.info;
@ -434,6 +460,12 @@ export const reducer = createReducer(
action.info.sentMessageJson || action.info.sentMessage
);
if (!finalEventMessage) {
/**
* .
*/
return state;
} else {
const roomInfo = {
...currentRoomInfo,
finalEventType: info.type,
@ -447,6 +479,7 @@ export const reducer = createReducer(
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }),
standbyRooms: fixedStandByRooms
};
}
} else {
return state;
}

View File

@ -1,9 +1,8 @@
import moment from 'moment';
import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { RoomUserDetailData, RoomUserData } from '@ucap/protocol-sync';
import {
RoomInfo,
UserInfo as RoomUserInfo,
@ -134,16 +133,23 @@ export function selectors<S>(selector: Selector<any, State>) {
selector,
(state: State) => state.standbyRooms
),
unreadTotal: createSelector(
selectRooms,
selectAllForRoom,
(roomState: RoomState, rooms: RoomInfo[]) => {
unreadTotal: createSelector(selectRooms, (roomState: RoomState) => {
let unreadTotal = 0;
for (const room of rooms) {
// tslint:disable-next-line: forin
for (const key in roomState.ids) {
const roomId = roomState.ids[key];
if (roomId === undefined) {
continue;
}
if (roomState.entities.hasOwnProperty(roomId)) {
const room = roomState.entities[roomId];
unreadTotal += room.noReadCnt;
}
return unreadTotal;
}
)
return unreadTotal;
})
};
}

View File

@ -128,8 +128,13 @@ export class ChatUtil {
default:
{
const m = finalEventMessage as string;
if (eventType === EventType.Character && m.trim().length === 0) {
eventMessage = '최근 대화 없음';
} else {
eventMessage = m;
}
}
break;
}

View File

@ -10,6 +10,9 @@ export * from './lib/config/module-config';
export { CommonActions, RoomActions, ChattingActions };
export * from './lib/utils/chat.util';
export * from './lib/store/state';
export { Chatting } from './lib/store/chatting/state';
export { RoomUserMap, RoomUserShortMap } from './lib/store/room/state';
export * from './lib/chat-store.module';

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-store-group",
"version": "0.0.14",
"version": "0.0.22",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -1,6 +1,6 @@
import { createAction, props } from '@ngrx/store';
import { UserInfo } from '@ucap/protocol-sync';
import { UserInfo } from '@ucap/protocol-sync';
import {
AddRequest as BuddyAddRequest,
AddResponse as BuddyAddResponse,
@ -11,7 +11,8 @@ import {
} from '@ucap/protocol-buddy';
import {
UserNicknameRequest as NicknameRequest,
UserNicknameResponse as NicknameResponse
UserNicknameResponse as NicknameResponse,
UserNotification
} from '@ucap/protocol-info';
/**
@ -142,3 +143,10 @@ export const nicknameFailure = createAction(
'[ucap::group::buddy] user nickname Failure',
props<{ error: any }>()
);
/**
* buddy info update by Notification
*/
export const buddyInfoUpdate = createAction(
'[ucap::group::buddy] buddy info update notification',
props<{ noti: UserNotification }>()
);

View File

@ -30,8 +30,11 @@ import { BuddyProtocolService } from '@ucap/ng-protocol-buddy';
import {
DepartmentSelector,
PresenceActions
PresenceActions,
CommonActions,
UserSelector
} from '@ucap/ng-store-organization';
import { LoginActions } from '@ucap/ng-store-authentication';
import * as groupActions from '../group/actions';
@ -51,7 +54,8 @@ import {
nickname,
nicknameSuccess,
nicknameFailure,
delAndClear
delAndClear,
buddyInfoUpdate
} from './actions';
import { BuddySelector, GroupSelector } from '../state';
@ -135,7 +139,7 @@ export class Effects {
),
tap(([req, groupList, myDeptUserList]) => {
for (const group of groupList) {
if (group.userSeqs.indexOf(req.seq as any) > -1) {
if (group.userSeqs.indexOf(String(req.seq)) > -1) {
// 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다.
if (
!!this.moduleConfig.useMyDeptGroup &&
@ -154,7 +158,7 @@ export class Effects {
groupSeq: group.seq,
groupName: group.name,
userSeqs: group.userSeqs.filter(
(userSeq) => userSeq !== (req.seq as any)
(userSeq) => userSeq !== String(req.seq)
)
}
})
@ -169,7 +173,7 @@ export class Effects {
this.moduleConfig.useMyDeptGroup &&
!!myDeptUserList &&
myDeptUserList.filter(
(deptUser) => deptUser.seq === (req.seq as any)
(deptUser) => deptUser.seq === String(req.seq)
).length > 0
) {
// skip;;
@ -268,25 +272,42 @@ export class Effects {
const userSeqsForDelete = action.userSeqsForDelete;
if (!!targetUserSeqs && 0 < targetUserSeqs.length) {
const addBuddyList: string[] = [];
targetUserSeqs.forEach((userSeq) => {
if (!buddyList) {
addBuddyList.push(userSeq);
return;
}
// Add Buddy
const userSeqsForAdd: string[] = [];
const index = buddyList.findIndex(
(b) => b.seq === Number(userSeq)
targetUserSeqs.map((seq) => {
const findBuddy = buddyList.filter(
(user) => Number(user.seq) === Number(seq)
);
if (-1 < index) {
addBuddyList.push(userSeq);
return;
if (!!findBuddy && findBuddy.length === 0) {
userSeqsForAdd.push(seq);
}
});
if (addBuddyList.length > 0) {
this.store.dispatch(add({ req: { userSeqs: addBuddyList } }));
if (userSeqsForAdd.length > 0) {
this.store.dispatch(add({ req: { userSeqs: userSeqsForAdd } }));
}
// const addBuddyList: string[] = [];
// targetUserSeqs.forEach((userSeq) => {
// if (!buddyList) {
// addBuddyList.push(userSeq);
// return;
// }
// const index = buddyList.findIndex(
// (b) => b.seq === Number(userSeq)
// );
// if (-1 < index) {
// addBuddyList.push(userSeq);
// return;
// }
// });
// if (addBuddyList.length > 0) {
// this.store.dispatch(add({ req: { userSeqs: addBuddyList } }));
// }
}
if (!!userSeqsForDelete && 0 < userSeqsForDelete.length) {
@ -304,6 +325,21 @@ export class Effects {
{ dispatch: false }
);
userNotificationForBuddy$ = createEffect(
() => {
return this.actions$.pipe(
ofType(CommonActions.userNotification),
withLatestFrom(this.store.pipe(select(UserSelector.user))),
tap(([action, user]) => {
if (Number(action.noti.SENDER_SEQ) !== Number(user.info.seq)) {
this.store.dispatch(buddyInfoUpdate({ noti: action.noti }));
}
})
);
},
{ dispatch: false }
);
constructor(
private actions$: Actions,
private store: Store<any>,

View File

@ -1,5 +1,6 @@
import { createReducer, on } from '@ngrx/store';
import { UserInfoUpdateType } from '@ucap/protocol-info';
import { UserInfo } from '@ucap/protocol-sync';
import { initialState, adapterBuddy } from './state';
@ -7,7 +8,8 @@ import {
buddy2Success,
delSuccess,
updateSuccess,
nicknameSuccess
nicknameSuccess,
buddyInfoUpdate
} from './actions';
export const reducer = createReducer(
@ -56,5 +58,49 @@ export const reducer = createReducer(
...state,
buddies: adapterBuddy.upsertOne(userInfo, { ...state.buddies })
};
}),
on(buddyInfoUpdate, (state, action) => {
const noti = action.noti;
let buddyInfo: UserInfo;
switch (noti.type) {
case UserInfoUpdateType.Image:
{
const findIdx = noti.info.indexOf('ProfileImage');
let imgInfo: string = noti.info;
if (findIdx > -1) {
const startIdx = noti.info.indexOf('/', findIdx);
imgInfo = noti.info.substring(startIdx);
}
buddyInfo = {
...state.buddies.entities[noti.SENDER_SEQ],
profileImageFile: imgInfo
};
}
break;
case UserInfoUpdateType.Intro:
{
buddyInfo = {
...state.buddies.entities[noti.SENDER_SEQ],
intro: noti.info
};
}
break;
case UserInfoUpdateType.TelephoneVisible:
{
buddyInfo = {
...state.buddies.entities[noti.SENDER_SEQ]
};
}
break;
}
return {
...state,
buddies: adapterBuddy.upsertOne(buddyInfo, { ...state.buddies })
};
})
);

View File

@ -1,12 +1,13 @@
import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { UserInfo } from '@ucap/protocol-sync';
export interface BuddyState extends EntityState<UserInfo> {
syncDate: string;
}
export const adapterBuddy = createEntityAdapter<UserInfo>({
selectId: userInfo => userInfo.seq
selectId: (userInfo) => userInfo.seq
});
export interface State {
@ -38,7 +39,7 @@ export function selectors<S>(selector: Selector<any, State>) {
buddies: createSelector(selectBuddies, selectAllForBuddy),
buddySyncDate: createSelector(
selectBuddies,
buddyState => buddyState.syncDate
(buddyState) => buddyState.syncDate
)
};
}

View File

@ -20,7 +20,6 @@ import {
} from '@ucap/protocol-group';
import { GroupProtocolService } from '@ucap/ng-protocol-group';
import { SyncProtocolService } from '@ucap/ng-protocol-sync';
import { DepartmentSelector } from '@ucap/ng-store-organization';
@ -132,17 +131,20 @@ export class Effects {
return this.actions$.pipe(
ofType(updateMember),
withLatestFrom(
this.store.pipe(select(BuddySelector.buddies)),
this.store.pipe(select(GroupSelector.groups)),
this.store.pipe(select(DepartmentSelector.myDepartmentUserInfoList)) // 내 부서원 비교.
),
switchMap(([action, groupList, myDeptUserList]) => {
switchMap(([action, buddyList, groupList, myDeptUserList]) => {
const targetGroup = action.targetGroup;
const targetUserSeqs = action.targetUserSeqs as any;
const targetUserSeqs = action.targetUserSeqs;
// Del Buddy
let userSeqsForDelete: string[] = targetGroup.userSeqs.filter(
(v) => targetUserSeqs.indexOf(v + '') < 0
);
let userSeqsForDelete: string[] = targetGroup.userSeqs.filter((v) => {
if (!targetUserSeqs.includes(v)) {
return v;
}
});
// 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다.
if (

View File

@ -11,9 +11,9 @@
"@ucap/protocol-query": "@ucap/protocol-query",
"@ucap/protocol-status": "@ucap/protocol-status",
"@ucap/ng-api-external": "@ucap/ng-api-external",
"@ucap/ng-protocol-info": "@ucap/ng-protocol-info",
"@ucap/ng-protocol-query": "@ucap/ng-protocol-query",
"@ucap/ng-protocol-status": "@ucap/ng-protocol-status",
"@ucap/ng-store-authentication": "@ucap/ng-store-authentication"
"@ucap/ng-protocol-status": "@ucap/ng-protocol-status"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-store-organization",
"version": "0.0.8",
"version": "0.0.20",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -10,8 +10,8 @@
"@ucap/core": "~0.0.1",
"@ucap/protocol-query": "~0.0.1",
"@ucap/ng-api-external": "~0.0.1",
"@ucap/ng-protocol-info": "~0.0.1",
"@ucap/ng-protocol-query": "~0.0.1",
"@ucap/ng-store-authentication": "~0.0.1",
"tslib": "^1.10.0"
}
}

View File

@ -1,3 +1,10 @@
import { createAction } from '@ngrx/store';
import { createAction, props } from '@ngrx/store';
import { UserNotification } from '@ucap/protocol-info';
export const init = createAction('[ucap::organization::common] init');
export const userNotification = createAction(
'[ucap::organization::common] user Notification',
props<{ noti: UserNotification }>()
);

View File

@ -1,8 +1,49 @@
import { withLatestFrom, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';
import { select, Store } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { modifyInfoSuccess } from '../user/actions';
import { UserSelector } from '../state';
import { userNotification } from './actions';
@Injectable()
export class Effects {
constructor(private actions$: Actions) {}
userNotification$ = createEffect(
() => {
return this.actions$.pipe(
ofType(userNotification),
withLatestFrom(this.store.pipe(select(UserSelector.user))),
tap(([action, user]) => {
if (Number(action.noti.SENDER_SEQ) === Number(user.info.seq)) {
// my
const noti = action.noti;
const findIdx = noti.info.indexOf('ProfileImage');
let notInfo: string = noti.info;
if (findIdx > -1) {
const startIdx = noti.info.indexOf('/', findIdx);
notInfo = noti.info.substring(startIdx);
}
this.store.dispatch(
modifyInfoSuccess({
res: {
SENDER_SEQ: noti.SENDER_SEQ,
info: notInfo,
type: noti.type
}
})
);
}
})
);
},
{ dispatch: false }
);
constructor(private actions$: Actions, private store: Store<any>) {}
}

View File

@ -1,5 +1,5 @@
import { Selector, createSelector } from '@ngrx/store';
import { DeptInfo, UserInfoSS } from '@ucap/protocol-query';
import { Company } from '@ucap/api-external';
export interface State {

View File

@ -7,7 +7,8 @@ import { Store } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { QueryProtocolService } from '@ucap/ng-protocol-query';
import { LoginActions } from '@ucap/ng-store-authentication';
import { init as userInit } from '../user/actions';
import {
dept,
@ -23,12 +24,12 @@ import { DeptDivisionCode } from '@ucap/protocol-query';
export class Effects {
sessionCreatedForGroups$ = createEffect(() => {
return this.actions$.pipe(
ofType(LoginActions.sessionCreated),
map(action =>
ofType(userInit),
map((action) =>
dept({
req: {
divCd: DeptDivisionCode.Organization,
companyCode: action.loginSession.companyCode
companyCode: action.user.companyCode
}
})
)
@ -38,17 +39,13 @@ export class Effects {
dept$ = createEffect(() => {
return this.actions$.pipe(
ofType(dept),
map(action => action.req),
switchMap(req => {
map((action) => action.req),
switchMap((req) => {
return this.queryProtocolService.dept(req).pipe(
map(res => {
const departmentInfoList = res.departmentInfoList.sort((a, b) =>
a.order < b.order ? -1 : a.order > b.order ? 1 : 0
);
return deptSuccess({ departmentInfoList });
map((res) => {
return deptSuccess({ departmentInfoList: res.departmentInfoList });
}),
catchError(error => of(deptFailure({ error })))
catchError((error) => of(deptFailure({ error })))
);
})
);
@ -57,24 +54,13 @@ export class Effects {
myDeptUser$ = createEffect(() => {
return this.actions$.pipe(
ofType(myDeptUser),
map(action => action.req),
switchMap(req => {
map((action) => action.req),
switchMap((req) => {
return this.queryProtocolService.deptUser(req).pipe(
map(res => {
const userInfos = res.userInfos.sort((a, b) =>
a.order < b.order
? -1
: a.order > b.order
? 1
: a.name < b.name
? -1
: a.name > b.name
? 1
: 0
);
return myDeptUserSuccess({ userInfos });
map((res) => {
return myDeptUserSuccess({ userInfos: res.userInfos });
}),
catchError(error => of(myDeptUserFailure({ error })))
catchError((error) => of(myDeptUserFailure({ error })))
);
})
);

View File

@ -4,10 +4,12 @@ import { Effects as CommonEffects } from './common/effects';
import { Effects as CompanyEffects } from './company/effects';
import { Effects as DepartmentEffects } from './department/effects';
import { Effects as PresenceEffects } from './presence/effects';
import { Effects as UserEffects } from './user/effects';
export const effects: Type<any>[] = [
CommonEffects,
CompanyEffects,
DepartmentEffects,
PresenceEffects
PresenceEffects,
UserEffects
];

View File

@ -5,54 +5,59 @@ import {
StatusNotification,
StatusRequest,
StatusResponse,
MessageUpdateRequest
MessageUpdateRequest,
MessageUpdateResponse
} from '@ucap/protocol-status';
export const bulkInfo = createAction(
'[ucap::organization::presence] Bulk Info',
'[ucap::organization::presence] bulkInfo',
props<BulkInfoRequest>()
);
export const bulkInfoSuccess = createAction(
'[ucap::organization::presence] Bulk Info Success',
'[ucap::organization::presence] bulkInfo Success',
props<{ statusBulkInfoList: StatusBulkInfo[] }>()
);
export const bulkInfoFailure = createAction(
'[ucap::organization::presence] Bulk Info Failure',
'[ucap::organization::presence] bulkInfo Failure',
props<{ error: any }>()
);
export const statusNotification = createAction(
'[ucap::organization::presence] Status Notification',
'[ucap::organization::presence] statusNotification',
props<{ noti: StatusNotification }>()
);
export const status = createAction(
'[ucap::organization::presence] Status',
'[ucap::organization::presence] status',
props<{ req: StatusRequest }>()
);
export const statusSuccess = createAction(
'[ucap::organization::presence] Status Success',
'[ucap::organization::presence] statusSuccess',
props<{
res: StatusResponse;
}>()
);
export const statusFailure = createAction(
'[ucap::organization::presence] Status Failure',
'[ucap::organization::presence] statusFailure',
props<{ error: any }>()
);
export const changeMyIdleCheckTime = createAction(
'[ucap::organization::presence] Change MyIdleCheckTime',
'[ucap::organization::presence] changeMyIdleCheckTime',
props<{ checkTime: number }>()
);
export const messageUpdate = createAction(
'[ucap::organization::presence] status message update of Others',
'[ucap::organization::presence] messageUpdate',
props<{ req: MessageUpdateRequest }>()
);
export const messageUpdateSuccess = createAction(
'[ucap::organization::presence] messageUpdate Success',
props<{ res: MessageUpdateResponse }>()
);
export const messageUpdateFailure = createAction(
'[ucap::organization::presence] status message update of Others Failure',
'[ucap::organization::presence] messageUpdate Failure',
props<{ error: any }>()
);

View File

@ -1,5 +1,5 @@
import { of } from 'rxjs';
import { map, exhaustMap, catchError, tap, switchMap } from 'rxjs/operators';
import { map, exhaustMap, catchError, concatMap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
@ -12,10 +12,12 @@ import {
statusFailure,
bulkInfo,
bulkInfoSuccess,
bulkInfoFailure
bulkInfoFailure,
messageUpdate,
messageUpdateSuccess,
messageUpdateFailure
} from './actions';
import { StatusProtocolService } from '@ucap/ng-protocol-status';
import { StatusBulkInfo } from '@ucap/protocol-status';
@Injectable()
export class Effects {
@ -23,7 +25,7 @@ export class Effects {
() => {
return this.actions$.pipe(
ofType(bulkInfo),
switchMap((req) => {
concatMap((req) => {
return this.statusProtocolService.bulkInfo(req).pipe(
map((res) => {
this.store.dispatch(
@ -55,6 +57,21 @@ export class Effects {
)
);
messageUpdate$ = createEffect(() =>
this.actions$.pipe(
ofType(messageUpdate),
map((action) => action.req),
exhaustMap((req) => {
return this.statusProtocolService.messageUpdate(req).pipe(
map((res) => {
return messageUpdateSuccess({ res });
}),
catchError((error) => of(messageUpdateFailure({ error })))
);
})
)
);
constructor(
private actions$: Actions,
private store: Store<any>,

View File

@ -1,4 +1,11 @@
import { createReducer, on } from '@ngrx/store';
import {
StatusBulkInfo,
TerminalStatusInfo,
TerminalStatusNumber
} from '@ucap/protocol-status';
import { initialState, State, adapterStatusBulkInfo } from './state';
import {
bulkInfoSuccess,
@ -6,11 +13,6 @@ import {
statusSuccess,
bulkInfo
} from './actions';
import {
StatusBulkInfo,
TerminalStatusInfo,
TerminalStatusNumber
} from '@ucap/protocol-status';
export const reducer = createReducer(
initialState,

View File

@ -1,5 +1,6 @@
import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { StatusBulkInfo } from '@ucap/protocol-status';
export interface StatusBulkInfoState extends EntityState<StatusBulkInfo> {}
@ -42,11 +43,13 @@ export function selectors<S>(selector: Selector<any, State>) {
selectStatusBulkInfo,
ngeSelectEntitiesStatusBulkInfo
),
selectStatusBulkInfo: (userSeq: number) =>
createSelector(
selectStatusBulkInfo: createSelector(
selectStatusBulkInfo,
ngeSelectEntitiesStatusBulkInfo,
(_, entities) => (!!entities ? entities[userSeq] : undefined)
(statusBulkInfoState: StatusBulkInfoState, userSeq: number) => {
return (
statusBulkInfoState.entities && statusBulkInfoState.entities[userSeq]
);
}
),
statusBulkInfoProcessing: createSelector(
selector,

View File

@ -4,12 +4,14 @@ import { reducer as CommonReducer } from './common/reducers';
import { reducer as CompanyReducer } from './company/reducers';
import { reducer as DepartmentReducer } from './department/reducers';
import { reducer as PresenceReducer } from './presence/reducers';
import { reducer as UserReducer } from './user/reducers';
export function reducers(state: any | undefined, action: Action) {
return combineReducers({
common: CommonReducer,
company: CompanyReducer,
department: DepartmentReducer,
presence: PresenceReducer
presence: PresenceReducer,
user: UserReducer
})(state, action);
}

View File

@ -4,6 +4,7 @@ import * as CommonState from './common/state';
import * as CompanyState from './company/state';
import * as DepartmentState from './department/state';
import * as PresenceState from './presence/state';
import * as UserState from './user/state';
export const KEY_FEATURE = 'organization';
@ -12,6 +13,7 @@ export interface State {
company: CompanyState.State;
department: DepartmentState.State;
presence: PresenceState.State;
user: UserState.State;
}
export const Selector = createFeatureSelector<State>(KEY_FEATURE);
@ -31,3 +33,7 @@ export const DepartmentSelector = DepartmentState.selectors(
export const PresenceSelector = PresenceState.selectors(
createSelector(Selector, (state: State) => state.presence)
);
export const UserSelector = UserState.selectors(
createSelector(Selector, (state: State) => state.user)
);

View File

@ -0,0 +1,37 @@
import { createAction, props } from '@ngrx/store';
import { UserResponse, UserRequest, User } from '@ucap/protocol-info';
/**
* info user request
*/
export const init = createAction(
'[ucap::organization::user] init',
props<{ user: User }>()
);
/**
* modifyInfo user request
*/
export const modifyInfo = createAction(
'[ucap::organization::user] modifyInfo',
props<{ req: UserRequest }>()
);
/**
* Success of modifyInfo request
*/
export const modifyInfoSuccess = createAction(
'[ucap::organization::user] modifyInfo Success',
props<{
res: UserResponse;
}>()
);
/**
* Failure of modifyInfo request
*/
export const modifyInfoFailure = createAction(
'[ucap::organization::user] modifyInfo Failure',
props<{ error: any }>()
);

View File

@ -0,0 +1,37 @@
import { of } from 'rxjs';
import { map, exhaustMap, catchError } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { UserResponse } from '@ucap/protocol-info';
import { InfoProtocolService } from '@ucap/ng-protocol-info';
import { modifyInfo, modifyInfoSuccess, modifyInfoFailure } from './actions';
@Injectable()
export class Effects {
infoUser$ = createEffect(() =>
this.actions$.pipe(
ofType(modifyInfo),
map((action) => action.req),
exhaustMap((req) =>
this.infoProtocolService.user(req).pipe(
map((res: UserResponse) => {
return modifyInfoSuccess({
res
});
}),
catchError((error) => of(modifyInfoFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private infoProtocolService: InfoProtocolService
) {}
}

View File

@ -0,0 +1,89 @@
import { createReducer, on } from '@ngrx/store';
import { UserInfoUpdateType } from '@ucap/protocol-info';
import { MessageIndexType } from '@ucap/protocol-status';
import { messageUpdateSuccess } from '../presence/actions';
import { init, modifyInfoSuccess } from './actions';
import { State, initialState } from './state';
export const reducer = createReducer(
initialState,
on(init, (state, action) => {
return {
...state,
user: action.user
} as State;
}),
on(modifyInfoSuccess, (state, action) => {
let user = {
...state.user
};
switch (action.res.type) {
case UserInfoUpdateType.Image:
user = {
...user,
info: {
...user.info,
profileImageFile: action.res.info
}
};
break;
case UserInfoUpdateType.Intro:
user = {
...user,
info: {
...user.info,
intro: action.res.info
}
};
break;
case UserInfoUpdateType.TelephoneVisible:
break;
default:
break;
}
return {
...state,
user
};
}),
on(messageUpdateSuccess, (state, action) => {
let user = {
...state.user
};
switch (action.res.index) {
case MessageIndexType.First:
user = {
...user,
statusMessage1: action.res.statusMessage
};
break;
case MessageIndexType.Second:
user = {
...user,
statusMessage2: action.res.statusMessage
};
break;
case MessageIndexType.Third:
user = {
...user,
statusMessage3: action.res.statusMessage
};
break;
default:
break;
}
return {
...state,
user
} as State;
})
);

View File

@ -0,0 +1,20 @@
import { Selector, createSelector } from '@ngrx/store';
import { EntityState } from '@ngrx/entity';
import { StatusBulkInfo } from '@ucap/protocol-status';
import { User } from '@ucap/protocol-info';
export interface StatusBulkInfoState extends EntityState<StatusBulkInfo> {}
export interface State {
user: User | undefined;
}
export const initialState: State = {
user: undefined
};
export function selectors<S>(selector: Selector<any, State>) {
return {
user: createSelector(selector, (state: State) => state.user)
};
}

View File

@ -6,10 +6,17 @@ import * as CommonActions from './lib/store/common/actions';
import * as CompanyActions from './lib/store/company/actions';
import * as DepartmentActions from './lib/store/department/actions';
import * as PresenceActions from './lib/store/presence/actions';
import * as UserActions from './lib/store/user/actions';
export * from './lib/config/module-config';
export { CommonActions, CompanyActions, DepartmentActions, PresenceActions };
export {
CommonActions,
CompanyActions,
DepartmentActions,
PresenceActions,
UserActions
};
export * from './lib/store/state';

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-authentication",
"version": "0.0.25",
"version": "0.0.29",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -1,6 +1,6 @@
<div class="change-password-form">
<form name="changePasswordForm" [formGroup]="changePasswordForm" novalidate>
<mat-form-field>
<mat-form-field floatLabel="never" appearance="none">
<mat-label>{{ 'accounts.fieldCurrentPassword' | ucapI18n }}</mat-label>
<input
matInput
@ -8,11 +8,11 @@
[formControl]="currentLoginPwFormControl"
/>
</mat-form-field>
<mat-form-field>
<mat-form-field floatLabel="never" appearance="none">
<mat-label>{{ 'accounts.fieldNewPassword' | ucapI18n }}</mat-label>
<input matInput type="password" [formControl]="newLoginPwFormControl" />
</mat-form-field>
<mat-form-field>
<mat-form-field floatLabel="never" appearance="none">
<mat-label>{{ 'accounts.fieldNewPasswordConfirm' | ucapI18n }}</mat-label>
<input
matInput

View File

@ -7,11 +7,15 @@
[style.display]="!!fixedCompanyCode ? 'none' : 'block'"
>
<mat-form-field color="accent">
<mat-label>{{ 'login.fields.company' | ucapI18n }}</mat-label>
<mat-label>{{
'authentication:login.fields.company' | ucapI18n
}}</mat-label>
<mat-select
[formControl]="companyCodeFormControl"
[value]="companyCode || fixedCompanyCode"
placeholder="{{ 'login.labels.selectCompany' | ucapI18n }}"
placeholder="{{
'authentication:login.labels.selectCompany' | ucapI18n
}}"
>
<mat-option
*ngFor="let company of companyList"
@ -49,8 +53,10 @@
</g>
</svg>
</span>
<mat-form-field>
<mat-label>{{ 'login.fields.loginId' | ucapI18n }}</mat-label>
<mat-form-field floatLabel="never" appearance="none">
<mat-label>{{
'authentication:login.fields.loginId' | ucapI18n
}}</mat-label>
<input matInput [formControl]="loginIdFormControl" />
</mat-form-field>
</div>
@ -95,8 +101,10 @@
</g>
</svg>
</span>
<mat-form-field>
<mat-label>{{ 'login.fields.loginPw' | ucapI18n }}</mat-label>
<mat-form-field floatLabel="never" appearance="none">
<mat-label>{{
'authentication:login.fields.loginPw' | ucapI18n
}}</mat-label>
<input
matInput
type="password"
@ -115,7 +123,7 @@
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireCompany' | ucapI18n }}
{{ 'authentication:login.errors.requireCompany' | ucapI18n }}
</mat-error>
<mat-error
*ngIf="
@ -125,7 +133,7 @@
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireLoginId' | ucapI18n }}
{{ 'authentication:login.errors.requireLoginId' | ucapI18n }}
</mat-error>
<mat-error
*ngIf="
@ -135,10 +143,10 @@
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireLoginPw' | ucapI18n }}
{{ 'authentication:login.errors.requireLoginPw' | ucapI18n }}
</mat-error>
<mat-error *ngIf="loginFailed" class="ucap-authentication-login-error">
{{ 'login.errors.failed' | ucapI18n }}
{{ 'authentication:login.errors.failed' | ucapI18n }}
</mat-error>
</div>
@ -156,11 +164,11 @@
<ng-container
*ngIf="!processing && (!loginTry || !loginTry.remainTimeForNextTry)"
>
{{ 'login.labels.doLogin' | ucapI18n }}
{{ 'authentication:login.labels.doLogin' | ucapI18n }}
</ng-container>
<ng-container *ngIf="!!loginTry && !!loginTry.remainTimeForNextTry">
{{ 'login.errors.attemptsExceeded' | ucapI18n }}
{{ 'authentication:login.errors.attemptsExceeded' | ucapI18n }}
(
{{
moment

View File

@ -2,6 +2,10 @@
* Public API Surface of ui-authentication
*/
export * from './lib/components/change-password.component';
export * from './lib/components/login.component';
export * from './lib/config/module-config';
export * from './lib/config/token';
export * from './lib/authentication-ui.module';

View File

@ -5,12 +5,16 @@
"entryFile": "src/public-api.ts",
"styleIncludePaths": ["./src/assets/scss"],
"umdModuleIds": {
"moment": "moment",
"ngx-perfect-scrollbar": "ngx-perfect-scrollbar",
"@ucap/core": "@ucap/core",
"@ucap/api": "@ucap/api",
"@ucap/protocol-event": "@ucap/protocol-event",
"@ucap/protocol-room": "@ucap/protocol-room",
"@ucap/ng-logger": "@ucap/ng-logger",
"@ucap/ng-i18n": "@ucap/ng-i18n",
"@ucap/ng-ui": "@ucap/ng-ui"
"@ucap/ng-ui": "@ucap/ng-ui",
"@ucap/ng-ui-organization": "@ucap/ng-ui-organization"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-chat",
"version": "0.0.13",
"version": "0.0.72",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -3,25 +3,36 @@ import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatRippleModule } from '@angular/material/core';
import { MatTreeModule } from '@angular/material/tree';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { UiModule } from '@ucap/ng-ui';
import { OrganizationUiModule } from '@ucap/ng-ui-organization';
import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { ModuleConfig } from './config/module-config';
import { _MODULE_CONFIG } from './config/token';
import {
ImageListComponent,
ImageListNodeDirective
} from './components/image-list.component';
import {
FileListComponent,
FileListNodeDirective
} from './components/file-list.component';
import { ImageListItem01Component } from './components/image-list-item-01.component';
import { FileListItem01Component } from './components/file-list-item-01.component';
import {
RoomExpansionComponent,
RoomExpansionHeaderDirective,
@ -45,10 +56,19 @@ import { ReadHereComponent } from './components/message-box/read-here.component'
import { DateSplitterComponent } from './components/message-box/date-splitter.component';
import { SmsComponent } from './components/message-box/sms.component';
import { ReplyComponent } from './components/message-box/reply.component';
import {
ChatItemListComponent,
ChatItemListNodeDirective
} from './components/item-list.component';
const COMPONENTS = [
ImageListItem01Component,
FileListItem01Component,
FileListComponent,
ImageListComponent,
RoomExpansionComponent,
RoomListItem01Component,
ChatItemListComponent,
InformationComponent,
TextComponent,
@ -72,7 +92,13 @@ const COMPONENTS = [
];
const DIALOGS = [];
const PIPES = [];
const DIRECTIVES = [RoomExpansionHeaderDirective, RoomExpansionNodeDirective];
const DIRECTIVES = [
RoomExpansionHeaderDirective,
RoomExpansionNodeDirective,
FileListNodeDirective,
ImageListNodeDirective,
ChatItemListNodeDirective
];
const SERVICES = [];
@NgModule({
@ -86,7 +112,6 @@ export class ChatUiRootModule {}
imports: [
CommonModule,
FlexLayoutModule,
ScrollingModule,
MatBadgeModule,
MatButtonModule,
@ -95,10 +120,13 @@ export class ChatUiRootModule {}
MatRippleModule,
MatTreeModule,
MatMenuModule,
MatProgressBarModule,
MatTooltipModule,
PerfectScrollbarModule,
I18nModule,
OrganizationUiModule,
UiModule
],
exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES],

View File

@ -0,0 +1,95 @@
<div class="ucap-chat-file-list-item-01-container">
<div class="contents">
<!--파일명에 따라 doc exe hwp ppt xls zip 으로 추가되고 나머지 파일 명은 file로 기간이 만료된 파일은 그뒤에 disable도 추가-->
<!-- <div class="file-img" [ngClass]="fileInfo.FileExt"></div> -->
<div
[ngClass]="[
'mime-icon',
'light',
'ico-' + fileInfo.sentMessageJson.fileExt
]"
>
<div class="ico"></div>
</div>
<ul class="file-info">
<li class="file-name">
<span class="text-filename">{{
fileInfo.sentMessageJson.fileName
}}</span>
<span class="text-username">{{
senderInfo | ucapOrganizationTranslate: 'name'
}}</span>
</li>
<li class="file-bytes-date">
<span class="amount-bytes">{{
fileInfo.sentMessageJson.attachmentSize | ucapBytes
}}</span>
<span class="text-date">
<em>{{
fileInfo.sentMessageJson.attachmentRegDate | ucapDate: 'MM.DD'
}}</em>
<em *ngIf="!!fileRetentionPeriod && fileRetentionPeriod > 0">
~
{{
fileInfo.sentMessageJson.attachmentRegDate
| ucapDate: 'MM.DD':fileRetentionPeriodOptions
}}
</em>
</span>
</li>
<li
*ngIf="!!downloadTotalCount && !!downloadedCount"
class="file-list-amount"
(click)="onClickShowDownCheck()"
>
<strong>{{ downloadedCount }}</strong
>/{{ downloadTotalCount }}
</li>
<li class="file-check-box">
<mat-checkbox
*ngIf="!!checkable"
#checkbox
aria-label="check"
[checked]="checked"
(change)="onChangeCheck(checkbox.checked)"
(click)="$event.stopPropagation()"
>
</mat-checkbox>
</li>
</ul>
</div>
<div
class="progress"
*ngIf="!!fileDownloadItem"
[ngClass]="
!!fileDownloadItem &&
!!fileDownloadItem.downloadingProgress$ &&
(fileDownloadItem.downloadingProgress$ | async) > 0 &&
(fileDownloadItem.downloadingProgress$ | async) < 100
? 'over'
: ''
"
>
<button
mat-icon-button
aria-label="close"
class="btn-close"
color="accent"
style="display: none;"
>
<mat-icon color="accent">cancel</mat-icon>
</button>
<mat-progress-bar
color="accent"
mode="determinate"
[value]="fileDownloadItem.downloadingProgress$ | async"
></mat-progress-bar>
<div class="progress-info-area">
<span style="display: none;">{{
'common:file.downloading' | ucapI18n
}}</span>
<strong>{{ fileDownloadItem.downloadingProgress$ | async }}%</strong>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-file-list-item-01-container {
display: flex;
flex-direction: row;
align-items: center;
.mime-icon {
flex: 0 0 60px;
}
ul {
flex: 1 1 auto;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
li {
display: flex;
flex-direction: column;
}
}
}

View File

@ -4,7 +4,7 @@ import { DebugElement } from '@angular/core';
import { ProfileListItemComponent } from './profile-list-item.component';
describe('ucap::ui-organization::ProfileListItemComponent', () => {
describe('ucap::ucap::organization::ProfileListItemComponent', () => {
let component: ProfileListItemComponent;
let fixture: ComponentFixture<ProfileListItemComponent>;

View File

@ -0,0 +1,93 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from '@angular/core';
import { Subject } from 'rxjs';
import {
UserInfo as RoomUserInfo,
UserInfoShort as RoomUserInfoShort
} from '@ucap/protocol-room';
import { FileInfo } from '@ucap/protocol-file';
import { DateOptions } from '@ucap/ng-ui';
import { FileDownloadItem } from '@ucap/api';
@Component({
selector: 'ucap-chat-file-list-item-01',
templateUrl: './file-list-item-01.component.html',
styleUrls: ['./file-list-item-01.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileListItem01Component implements OnInit, OnDestroy {
@Input()
fileInfo: FileInfo;
@Input()
senderInfo: RoomUserInfo | RoomUserInfoShort;
@Input()
downloadTotalCount: number;
@Input()
downloadedCount: number;
@Input()
fileRetentionPeriod: number;
@Input()
expired = false;
@Input()
checked = false;
@Input()
checkable = true;
@Input()
fileDownloadItem: FileDownloadItem;
@Output()
changeCheck = new EventEmitter<{
checked: boolean;
fileInfo: FileInfo;
fileDownloadItem: FileDownloadItem;
}>();
@Output()
showDownCheck = new EventEmitter<FileInfo>();
private ngOnDestroySubject: Subject<void> = new Subject();
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
onClickShowDownCheck() {
this.showDownCheck.emit(this.fileInfo);
}
onChangeCheck(checked: boolean) {
this.changeCheck.emit({
checked,
fileInfo: this.fileInfo,
fileDownloadItem: this.fileDownloadItem
});
}
get fileRetentionPeriodOptions(): DateOptions {
return {
manipulate: { amount: this.fileRetentionPeriod, unit: 'days' }
};
}
}

View File

@ -0,0 +1,15 @@
<div class="ucap-chat-file-list-container" fxFlexFill>
<ucap-virtual-scroll-viewport
#cvsvList
perfectScrollbar
fxFlexFill
[itemSize]="vsItemSize"
>
<ng-container *ucapVirtualFor="let fileInfo of fileInfos">
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: fileInfo }"
></ng-container>
</ng-container>
</ucap-virtual-scroll-viewport>
</div>

View File

@ -0,0 +1,7 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-file-list-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { FileListComponent } from './file-list.component';
describe('ucap::chat::FileListComponent', () => {
let component: FileListComponent;
let fixture: ComponentFixture<FileListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FileListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { moduleMetadata } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ChatUiModule } from '../chat-ui.module';
import { FileListComponent } from './file-list.component';
export default {
title: 'ui-chat::FileListComponent',
component: FileListComponent,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, ChatUiModule],
providers: []
})
]
};
export const Default = () => ({
component: FileListComponent,
props: {}
});

View File

@ -0,0 +1,24 @@
@import '~@ucap/ng-ui-material/material';
@mixin ucap-organization-profile-list-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.ucap-organization-profile-list-container {
border-color: mat-color($foreground, secondary-text);
}
}
@mixin ucap-organization-profile-list-typography($config) {
.ucap-organization-profile-list-container {
.searchword-container {
.form-field-input-searchword {
font-family: mat-font-family($config);
}
}
}
}

View File

@ -0,0 +1,55 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Directive,
ContentChild,
TemplateRef
} from '@angular/core';
import { FileInfo } from '@ucap/protocol-file';
@Directive({
selector: '[ucapChatFileListNode]'
})
export class FileListNodeDirective {}
@Component({
selector: 'ucap-chat-file-list',
templateUrl: './file-list.component.html',
styleUrls: ['./file-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileListComponent implements OnInit, OnDestroy {
@Input()
fileInfos: FileInfo[];
@Input()
vsItemSize: number;
@ContentChild(FileListNodeDirective, {
read: TemplateRef,
static: false
})
nodeTemplate: TemplateRef<FileListNodeDirective>;
private ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject();
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -0,0 +1,92 @@
<div
class="ucap-chat-image-list-item-01-container"
(mouseover)="onMouseover($event)"
(mouseleave)="onMouseleave($event)"
>
<div class="contents">
<img
[src]="fileInfo.sentMessageJson.thumbUrl"
onerror="this.src='assets/sticker/sticker_default.png'"
/>
<mat-checkbox
#checkbox
*ngIf="!!checkable"
aria-label="check"
[checked]="checked"
(change)="onChangeCheck(checkbox.checked)"
(click)="$event.stopPropagation()"
>
</mat-checkbox>
</div>
<div class="btn-box" [ngClass]="!!showOverButtons ? 'over' : ''">
<ul>
<li>
<button
mat-icon-button
class="ico-btn-obj"
aria-label="저장"
[matTooltip]="'common:file.save' | ucapI18n"
(click)="onClickSave($event)"
>
<mat-icon>save_alt</mat-icon>
</button>
</li>
<li>
<button
mat-icon-button
class="ico-btn-obj"
aria-label="뷰어로열기"
[matTooltip]="'common:file.fileOpen' | ucapI18n"
(click)="onClickOpenViewer($event)"
>
<mat-icon>launch</mat-icon>
</button>
</li>
<li>
<button
mat-icon-button
class="ico-btn-obj"
aria-label="삭제"
[matTooltip]="'common:file.delete' | ucapI18n"
(click)="onClickDelete($event)"
>
<mat-icon class="material-icons-outlined">delete_outline</mat-icon>
</button>
</li>
</ul>
</div>
<div
class="progress"
*ngIf="!!fileDownloadItem"
[ngClass]="
!!fileDownloadItem &&
!!fileDownloadItem.downloadingProgress$ &&
(fileDownloadItem.downloadingProgress$ | async) > 0 &&
(fileDownloadItem.downloadingProgress$ | async) < 100
? 'over'
: ''
"
>
<button
mat-icon-button
aria-label="close"
class="btn-close"
color="accent"
style="display: none;"
>
<mat-icon color="accent">cancel</mat-icon>
</button>
<mat-progress-bar
color="accent"
mode="determinate"
[value]="fileDownloadItem.downloadingProgress$ | async"
></mat-progress-bar>
<div class="progress-info-area">
<span style="display: none;">{{
'common:file.downloading' | ucapI18n
}}</span>
<strong>{{ fileDownloadItem.downloadingProgress$ | async }}%</strong>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-image-list-item-01-container {
width: 100%;
height: 100%;
//padding-left: 4px;
//margin-left: -4px;
.contents {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
img {
}
.mat-checkbox {
position: absolute;
//right: 0;
//top: 0;
}
}
.btn-box {
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
//background-color: rgba(0, 0, 0, 0.5);
ul {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
height: 30px;
position: absolute;
//bottom: 0;
li {
button {
//width: 30%;
//height: 30px;
}
}
}
}
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ProfileListItemComponent } from './profile-list-item.component';
describe('ucap::ucap::organization::ProfileListItemComponent', () => {
let component: ProfileListItemComponent;
let fixture: ComponentFixture<ProfileListItemComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProfileListItemComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProfileListItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,122 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from '@angular/core';
import { Subject } from 'rxjs';
import { FileInfo } from '@ucap/protocol-file';
import { FileDownloadItem } from '@ucap/api';
import { FileEventJson } from '@ucap/protocol-event';
@Component({
selector: 'ucap-chat-image-list-item-01',
templateUrl: './image-list-item-01.component.html',
styleUrls: ['./image-list-item-01.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageListItem01Component implements OnInit, OnDestroy {
private ngOnDestroySubject: Subject<void> = new Subject();
@Input()
fileInfo: FileInfo;
@Input()
expired = false;
@Input()
checked = false;
@Input()
checkable = true;
@Input()
fileDownloadItem: FileDownloadItem;
@Output()
changeCheck = new EventEmitter<{
checked: boolean;
fileInfo: FileInfo;
fileDownloadItem: FileDownloadItem;
}>();
@Output()
save = new EventEmitter<{
fileInfo: FileEventJson;
fileDownloadItem: FileDownloadItem;
type: string;
}>();
@Output()
openViewer = new EventEmitter<FileInfo>();
@Output()
delete = new EventEmitter<FileInfo>();
showOverButtons = false;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
onMouseover(event: MouseEvent): void {
if (!this.expired) {
this.showOverButtons = true;
}
event.preventDefault();
event.stopPropagation();
}
onMouseleave(event: MouseEvent): void {
this.showOverButtons = false;
event.preventDefault();
event.stopPropagation();
}
onChangeCheck(checked: boolean) {
this.changeCheck.emit({
checked,
fileInfo: this.fileInfo,
fileDownloadItem: this.fileDownloadItem
});
}
onClickSave(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
const param = {
fileInfo: {
// require parameters.
fileName: this.fileInfo.name,
attachmentSeq: this.fileInfo.seq
} as FileEventJson,
fileDownloadItem: this.fileDownloadItem,
type: 'save'
};
this.save.emit(param);
}
onClickOpenViewer(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
this.openViewer.emit(this.fileInfo);
}
onClickDelete(event: MouseEvent) {
event.stopPropagation();
event.preventDefault();
this.delete.emit(this.fileInfo);
}
}

View File

@ -0,0 +1,15 @@
<div class="ucap-chat-image-list-container" fxFlexFill>
<ucap-virtual-scroll-viewport
#cvsvList
perfectScrollbar
fxFlexFill
[itemSize]="vsItemSize"
>
<ng-container *ucapVirtualFor="let fileInfo of fileInfos">
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: fileInfo }"
></ng-container>
</ng-container>
</ucap-virtual-scroll-viewport>
</div>

View File

@ -0,0 +1,7 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-image-list-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ImageListComponent } from './image-list.component';
describe('ucap::chat::ImageListComponent', () => {
let component: ImageListComponent;
let fixture: ComponentFixture<ImageListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ImageListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImageListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { moduleMetadata } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ChatUiModule } from '../chat-ui.module';
import { ImageListComponent } from './image-list.component';
export default {
title: 'ui-chat::ImageListComponent',
component: ImageListComponent,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, ChatUiModule],
providers: []
})
]
};
export const Default = () => ({
component: ImageListComponent,
props: {}
});

View File

@ -0,0 +1,24 @@
@import '~@ucap/ng-ui-material/material';
@mixin ucap-organization-profile-list-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.ucap-organization-profile-list-container {
border-color: mat-color($foreground, secondary-text);
}
}
@mixin ucap-organization-profile-list-typography($config) {
.ucap-organization-profile-list-container {
.searchword-container {
.form-field-input-searchword {
font-family: mat-font-family($config);
}
}
}
}

View File

@ -0,0 +1,55 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Directive,
ContentChild,
TemplateRef
} from '@angular/core';
import { FileInfo } from '@ucap/protocol-file';
@Directive({
selector: '[ucapChatImageListNode]'
})
export class ImageListNodeDirective {}
@Component({
selector: 'ucap-chat-image-list',
templateUrl: './image-list.component.html',
styleUrls: ['./image-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImageListComponent implements OnInit, OnDestroy {
@Input()
fileInfos: FileInfo[];
@Input()
vsItemSize: number;
@ContentChild(ImageListNodeDirective, {
read: TemplateRef,
static: false
})
nodeTemplate: TemplateRef<ImageListNodeDirective>;
private ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject();
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -0,0 +1,17 @@
<div class="ucap-chat-item-list-container" #container fxFlexFill>
<ucap-virtual-scroll-viewport
#cvsvList
fxFlexFill
perfectScrollbar
[itemSize]="vsItemSize"
>
<ng-container *ucapVirtualFor="let itemInfo of itemInfos">
<div [style.height.px]="vsItemSize">
<ng-template
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: itemInfo }"
></ng-template>
</div>
</ng-container>
</ucap-virtual-scroll-viewport>
</div>

View File

@ -0,0 +1,11 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-item-list-container {
width: 100%;
height: 100%;
ucap-virtual-scroll-viewport {
width: 100%;
height: 100%;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ChatItemListComponent } from './item-list.component';
describe('ucap::organization::ChatItemListComponent', () => {
let component: ChatItemListComponent;
let fixture: ComponentFixture<ChatItemListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChatItemListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChatItemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,22 @@
import { moduleMetadata } from '@storybook/angular';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ChatItemListComponent } from './item-list.component';
export default {
title: 'ui-chat::ChatItemListComponent',
component: ChatItemListComponent,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule],
providers: []
})
]
};
export const Default = () => ({
component: ChatItemListComponent,
props: {}
});

View File

@ -0,0 +1,24 @@
@import '~@ucap/ng-ui-material/material';
@mixin ucap-chat-item-list-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.ucap-chat-item-list-container {
border-color: mat-color($foreground, secondary-text);
}
}
@mixin ucap-chat-item-list-typography($config) {
.ucap-chat-item-list-container {
.searchword-container {
.form-field-input-searchword {
font-family: mat-font-family($config);
}
}
}
}

View File

@ -0,0 +1,71 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Directive,
ContentChild,
TemplateRef,
ViewChild,
ElementRef
} from '@angular/core';
import { ObjectUtil } from '@ucap/core';
import { UserInfo, UserInfoShort, RoomInfo } from '@ucap/protocol-room';
import { VirtualScrollViewportComponent } from '@ucap/ng-ui';
export type ItemInfoTypes = UserInfo | UserInfoShort | RoomInfo;
@Directive({
selector: '[ucapChatItemListNode]'
})
export class ChatItemListNodeDirective {}
@Component({
selector: 'ucap-chat-item-list',
templateUrl: './item-list.component.html',
styleUrls: ['./item-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatItemListComponent implements OnInit, OnDestroy {
@Input()
itemInfos: ItemInfoTypes[];
@Input()
vsItemSize: number;
@ContentChild(ChatItemListNodeDirective, {
read: TemplateRef,
static: false
})
nodeTemplate: TemplateRef<ChatItemListNodeDirective>;
@ViewChild('container', { static: true })
container: ElementRef<HTMLElement>;
@ViewChild('cvsvList', { static: true })
cvsvList: VirtualScrollViewportComponent;
ObjectUtil = ObjectUtil;
private ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject();
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -1,4 +1,10 @@
<div class="ucap-chat-message-box-attach-file" (click)="onClickOpenViewer()">
<div class="chat-bubble-area">
<div
class="ucap-chat-message-box-attach-file-bubble"
(mouseenter)="mouseEnter($event)"
(mouseleave)="mouseLeave($event)"
>
<div class="file-info-box" (click)="onClickOpenViewer()">
<!--파일명에 따라 doc exe hwp ppt xls zip 으로 추가되고 나머지 파일 명은 file로 기간이 만료된 파일은 그뒤에 disable도 추가-->
<!-- <div class="file-img" [ngClass]="fileInfo.FileExt"></div> -->
<div [ngClass]="['mime-icon', 'light', 'ico-' + fileInfo.fileExt]">
@ -10,28 +16,113 @@
</li>
<li class="date">
{{ fileInfo.attachmentRegDate | ucapDate }}
<span *ngIf="!!fileRetentionPeriod && fileRetentionPeriod > 0">
~
{{
fileInfo.attachmentRegDate
| ucapDate: 'YYYY.MM.DD':fileRetentionPeriodOptions
}}
</span>
</li>
</ul>
</div>
<div class="btn-box over">
<ul *ngIf="!expired">
<li>
<button mat-button (click)="onClickSave()">
저장
<div class="btn-box" [ngClass]="!!showOverButtons ? 'over' : ''">
<ul>
<li *ngIf="!existFile">
<button
mat-flat-button
color="accent"
class="btn-file-ctrl"
(click)="onClickSave()"
aria-label="저장"
[matTooltip]="'common:file.save' | ucapI18n"
>
<mat-icon>save_alt</mat-icon>
</button>
</li>
<li *ngIf="!existFile">
<button
mat-flat-button
*ngIf="deviceType !== DeviceType.Web"
color="accent"
class="btn-file-ctrl"
(click)="onClickSaveAs()"
aria-label="다른 이름 저장"
[matTooltip]="'common:file.saveAs' | ucapI18n"
>
<mat-icon>save</mat-icon>
</button>
</li>
<li *ngIf="!!existFile">
<button
mat-flat-button
*ngIf="deviceType !== DeviceType.Web"
color="accent"
class="btn-file-ctrl"
(click)="onClickOpenFile()"
aria-label="파일열기"
[matTooltip]="'common:file.fileOpen' | ucapI18n"
>
<mat-icon class="material-icons-outlined">text_snippet</mat-icon>
</button>
</li>
<li *ngIf="!!existFile">
<button
mat-flat-button
*ngIf="deviceType !== DeviceType.Web"
color="accent"
class="btn-file-ctrl"
(click)="onClickOpenFolder()"
aria-label="폴더열기"
[matTooltip]="'common:file.folderOpen' | ucapI18n"
>
<mat-icon class="material-icons-outlined">folder</mat-icon>
</button>
</li>
<li>
<button mat-button (click)="onClickSave()">
폴더열기
</button>
</li>
<li>
<button mat-button (click)="onClickSave()">
???
<button
mat-flat-button
color="accent"
class="btn-file-ctrl"
(click)="onClickOpenViewer()"
aria-label="뷰어 열기"
[matTooltip]="'label.openViewer' | ucapI18n"
>
<mat-icon>open_in_new</mat-icon>
</button>
</li>
</ul>
</div>
<div class="progress over">
프로그레스 영역
<div
class="progress"
[ngClass]="
!!fileDownloadItem &&
!!fileDownloadItem.downloadingProgress$ &&
(fileDownloadItem.downloadingProgress$ | async) > 0 &&
(fileDownloadItem.downloadingProgress$ | async) < 100
? 'over'
: ''
"
>
<button
mat-icon-button
aria-label="close"
class="btn-close"
color="accent"
style="display: none;"
>
<mat-icon color="accent">cancel</mat-icon>
</button>
<mat-progress-bar
color="accent"
mode="determinate"
[value]="fileDownloadItem.downloadingProgress$ | async"
></mat-progress-bar>
<div class="progress-info-area">
<span>{{ 'common:file.downloading' | ucapI18n }}</span
><strong>{{ fileDownloadItem.downloadingProgress$ | async }}%</strong>
</div>
</div>
</div>
</div>

View File

@ -1,126 +1,124 @@
// $tablet-s-width: 768px;
$tablet-s-width: 768px;
// .bubble-main {
// display: flex;
// flex-direction: row;
// padding: 14px;
// min-width: 300px;
// .file-img {
// display: inline-flex;
// width: 50px;
// height: 50px;
// float: left;
// margin-right: 14px;
// background-repeat: no-repeat;
// &.doc {
// background-image: url(/assets/images/file/icon_talk_doc.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_doc_d.png);
// }
// }
// &.exe {
// background-image: url(/assets/images/file/icon_talk_exe.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_exe_d.png);
// }
// }
// &.file {
// background-image: url(/assets/images/file/icon_talk_file.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_file_d.png);
// }
// }
// &.hwp {
// background-image: url(/assets/images/file/icon_talk_hwp.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_hwp_d.png);
// }
// }
// &.ppt,
// &.pptx {
// background-image: url(/assets/images/file/icon_talk_ppt.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_ppt_d.png);
// }
// }
// &.xls,
// &.xlsm,
// &.xlsx {
// background-image: url(/assets/images/file/icon_talk_xls.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_xls_d.png);
// }
// }
// &.zip {
// background-image: url(/assets/images/file/icon_talk_zip.png);
// &.disable {
// background-image: url(/assets/images/file/icon_talk_doc_d.png);
// }
// }
// }
// .file-info {
// display: inline-flex;
// flex-direction: column;
// text-align: left;
// float: left;
// line-height: 1.6em;
// .file-name {
// font-size: 1em;
// font-weight: bold;
// display: inline-flex;
// }
// .file-size {
// display: inline-flex;
// font-size: 11px;
// color: #666666;
// }
// .file-ext {
// font-size: 12px;
// color: #848d95;
// }
// }
// }
// .btn-box {
// width: 100%;
// height: 40px;
// border-top: 1px solid #dddddd;
// display: flex;
// width: 100%;
// text-align: center;
// font-size: 1em;
// ul {
// width: 100%;
// li {
// width: 50%;
// height: 100%;
// display: inline-block;
// text-align: center;
.ucap-chat-message-box-attach-file-bubble {
display: flex;
flex-direction: row;
align-items: center;
//padding: 6px 12px 0 0;
//border-radius: 2px;
cursor: pointer;
.file-info-box {
display: flex;
flex-direction: row;
//align-items: center;
// border-right: 1px solid #dddddd;
// @media screen and (max-width: #{$tablet-s-width}) {
// width: 30%;
// }
// &:last-child {
// border-right: none;
// @media screen and (max-width: #{$tablet-s-width}) {
// width: 70%;
// }
// }
// .mat-button {
// width: 100%;
// display: block;
// height: 100%;
// font-size: 1em;
// }
// }
// &.expired {
// li {
// width: 100%;
// white-space: nowrap;
// color: #999999;
// align-items: center;
// line-height: 40px;
// }
// }
// }
// }
//padding: 6px 12px 0 0;
//border-radius: 2px;
cursor: pointer;
.file-info {
//border-radius: 2px;
.file-name {
font-weight: 600;
//line-height: 1.2;
//font-size: 0.929em;
}
.date {
//color: #c92b5c;
//font-size: 0.875em;
//margin-top: 4px;
}
}
}
.btn-box {
width: 100%;
height: 100%;
//background-color: rgba(0, 0, 0, 0.6);
display: none;
//transition: all 0.2s linear;
//opacity: 0;
//border-radius: 2px;
cursor: default;
&.over {
position: absolute;
display: block;
z-index: 5;
top: 0;
left: 0;
//transition: all 0.3s linear;
opacity: 1;
}
ul {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: center;
width: 100%;
height: 100%;
li {
//padding-left: 16px;
&:first-child {
padding-left: 0;
}
.btn-file-ctrl {
//@include matbtnCtrl(36px, 36px, 50%, 36px);
width: 36px;
}
}
}
}
.progress {
//display: flex;
display: none;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
//background-color: rgba(0, 0, 0, 0.6);
padding: 0 20px;
border-radius: 2px;
cursor: default;
&:before {
content: '';
//width: 16px;
// height: 16px;
position: absolute;
z-index: 0;
//right: 22px;
//top: calc(50% - 25px);
//background-color: $white;
display: inline-block;
//border-radius: 50%;
}
&.over {
display: flex;
position: absolute;
z-index: 5;
top: 0;
left: 0;
}
.btn-close {
align-self: flex-end;
//@include matbtnCtrl(20px, 20px, 0, 20px);
// width: 20px;
//margin-bottom: 6px;
}
.progress-info-area {
display: flex;
flex-direction: row;
justify-content: space-between;
//color: $white;
//font-size: 0.875em;
//margin-top: 4px;
//font-weight: 600;
}
}
.desibled {
.ucap-file-bubble {
.ucap-chat-message-box-attach-file {
// color: #999 !important;
// background: #f1f2f6 !important;
}
}
}
}

View File

@ -1,5 +1,8 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FileEventJson } from '@ucap/protocol-event';
import { DateOptions } from '@ucap/ng-ui';
import { FileDownloadItem } from '@ucap/api';
import { DeviceType } from '@ucap/core';
@Component({
selector: 'ucap-chat-message-box-attach-file',
@ -10,26 +13,83 @@ export class AttachFileComponent implements OnInit {
@Input()
fileInfo: FileEventJson;
@Input()
fileDownloadItem: FileDownloadItem;
@Input()
existFile = false;
@Input()
deviceType: DeviceType;
@Input()
expired = false;
@Input()
fileRetentionPeriod?: number;
@Output()
save = new EventEmitter<string>();
@Output()
openFile = new EventEmitter();
@Output()
openFolder = new EventEmitter();
@Output()
openViewer = new EventEmitter();
showOverButtons = false;
DeviceType = DeviceType;
constructor() {}
ngOnInit() {}
onClickSave() {
if (!this.expired) {
this.save.emit('save');
}
}
onClickSaveAs() {
if (!this.expired) {
this.save.emit('saveAs');
}
}
onClickOpenFile() {
if (!this.expired) {
this.openFile.emit();
}
}
onClickOpenFolder() {
if (!this.expired) {
this.openFolder.emit();
}
}
onClickOpenViewer() {
if (!this.expired) {
this.openViewer.emit();
}
}
mouseEnter(event: MouseEvent): void {
if (!this.expired) {
this.showOverButtons = true;
}
event.stopPropagation();
event.preventDefault();
}
mouseLeave(event: MouseEvent): void {
this.showOverButtons = false;
event.stopPropagation();
event.preventDefault();
}
get fileRetentionPeriodOptions(): DateOptions {
return {
manipulate: { amount: this.fileRetentionPeriod, unit: 'days' }
};
}
}

View File

@ -0,0 +1,31 @@
@import '~@ucap/ng-ui-material/material';
.ucap-chat-message-box-date-splitter {
display: flex;
align-content: center;
flex-flow: row;
//height: 30px;
position: relative;
justify-content: center;
//margin: 20px 0;
&:before {
content: '';
// height: 1px;
// background-color: $gray-re3;
width: 100%;
position: absolute;
z-index: 3;
// top: 50%;
}
.date {
min-width: 160px;
// font-size: 0.979em;
// text-align: center;
// font-weight: 600;
// color: $gray-re3;
position: relative;
z-index: 5;
// padding: 0 36px;
// align-self: center;
}
}

View File

@ -6,8 +6,14 @@
<ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.File"
[fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)"
>
</ucap-chat-message-box-attach-file>
@ -15,8 +21,14 @@
<ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.Sound"
[fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)"
>
</ucap-chat-message-box-attach-file>
@ -25,6 +37,7 @@
*ngSwitchCase="FileType.Image"
[fileInfo]="fileInfo"
[expired]="getExpiredFile()"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)"
></ucap-chat-message-box-image>
@ -32,8 +45,14 @@
<ucap-chat-message-box-video
*ngSwitchCase="FileType.Video"
[fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)"
></ucap-chat-message-box-video>

View File

@ -2,6 +2,8 @@ import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core';
import { FileType, Info, FileEventJson } from '@ucap/protocol-event';
import { RoomInfo } from '@ucap/protocol-room';
import { FileDownloadItem, StatusCode } from '@ucap/api';
import { SelectFileInfo } from '@ucap/ng-ui';
import { DeviceType } from '@ucap/core';
@Component({
selector: 'ucap-chat-message-box-file',
@ -15,14 +17,30 @@ export class FileComponent implements OnInit {
@Input()
roomInfo: RoomInfo;
@Input()
existFile = false;
@Input()
deviceType?: DeviceType;
@Input()
fileRetentionPeriod?: number;
@Output()
save = new EventEmitter<{
fileInfo: FileEventJson;
fileDownloadItem: FileDownloadItem;
type: string;
}>();
@Output()
fileViewer = new EventEmitter<FileEventJson>();
fileViewer = new EventEmitter<SelectFileInfo>();
@Output()
openFile = new EventEmitter<FileEventJson>();
@Output()
openFolder = new EventEmitter<FileEventJson>();
fileInfo?: FileEventJson;
fileDownloadItem: FileDownloadItem;
@ -55,7 +73,7 @@ export class FileComponent implements OnInit {
onClickFileViewer(fileInfo: FileEventJson) {
if (!this.getExpiredFile()) {
this.fileViewer.emit(fileInfo);
this.fileViewer.emit({ attachmentSeq: this.fileInfo.attachmentSeq });
}
}
@ -68,4 +86,14 @@ export class FileComponent implements OnInit {
});
}
}
onClickOpenFile(fileInfo: FileEventJson): void {
if (!this.getExpiredFile()) {
this.openFile.emit(fileInfo);
}
}
onClickOpenFolder(fileInfo: FileEventJson): void {
if (!this.getExpiredFile()) {
this.openFolder.emit(fileInfo);
}
}
}

Some files were not shown because too many files have changed in this diff Show More