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": { "store-authentication": {
"projectType": "library", "projectType": "library",
"root": "projects/store-authentication", "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:pi": "node ./scripts/build.js pi",
"build:core": "node ./scripts/build.js core", "build:core": "node ./scripts/build.js core",
"build:logger": "node ./scripts/build.js logger", "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": "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: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": "node ./scripts/build.js protocol",
"build:protocol-authentication": "node ./scripts/build.js protocol-authentication", "build:protocol-authentication": "node ./scripts/build.js protocol-authentication",
@ -65,9 +64,8 @@
"publish:pi": "cd ./dist/pi && npm publish", "publish:pi": "cd ./dist/pi && npm publish",
"publish:core": "cd ./dist/core && npm publish", "publish:core": "cd ./dist/core && npm publish",
"publish:logger": "cd ./dist/logger && 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": "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: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": "cd ./dist/protocol && npm publish",
"publish:protocol-authentication": "cd ./dist/protocol-authentication && npm publish", "publish:protocol-authentication": "cd ./dist/protocol-authentication && npm publish",
@ -110,35 +108,33 @@
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook" "build-storybook": "build-storybook"
}, },
"dependencies": { "dependencies": {},
"@storybook/addon-knobs": "^5.3.18",
"tslib": "^1.10.0"
},
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.900.6", "@angular-devkit/build-angular": "^0.900.6",
"@angular-devkit/build-ng-packagr": "^0.900.6", "@angular-devkit/build-ng-packagr": "^0.900.6",
"@angular/animations": "^9.0.6", "@angular/animations": "^9.1.11",
"@angular/cdk": "^9.1.2", "@angular/cdk": "^9.2.4",
"@angular/cli": "^9.0.6", "@angular/cli": "^9.1.9",
"@angular/common": "^9.0.6", "@angular/common": "^9.1.11",
"@angular/compiler": "^9.0.6", "@angular/compiler": "^9.1.11",
"@angular/compiler-cli": "^9.0.6", "@angular/compiler-cli": "^9.1.11",
"@angular/core": "^9.0.6", "@angular/core": "^9.1.11",
"@angular/flex-layout": "^9.0.0-beta.29", "@angular/flex-layout": "^9.0.0-beta.31",
"@angular/forms": "^9.0.6", "@angular/forms": "^9.1.11",
"@angular/language-service": "^9.0.6", "@angular/language-service": "^9.1.11",
"@angular/material": "^9.1.2", "@angular/material": "^9.2.4",
"@angular/material-moment-adapter": "^9.1.2", "@angular/material-moment-adapter": "^9.2.4",
"@angular/platform-browser": "^9.0.6", "@angular/platform-browser": "^9.1.11",
"@angular/platform-browser-dynamic": "^9.0.6", "@angular/platform-browser-dynamic": "^9.1.11",
"@angular/router": "^9.0.6", "@angular/router": "^9.1.11",
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@ngrx/effects": "^9.0.0", "@ngrx/effects": "^9.2.0",
"@ngrx/entity": "^9.0.0", "@ngrx/entity": "^9.2.0",
"@ngrx/router-store": "^9.0.0", "@ngrx/router-store": "^9.2.0",
"@ngrx/store": "^9.0.0", "@ngrx/store": "^9.2.0",
"@ngrx/store-devtools": "^9.0.0", "@ngrx/store-devtools": "^9.2.0",
"@storybook/addon-actions": "^5.3.18", "@storybook/addon-actions": "^5.3.18",
"@storybook/addon-knobs": "^5.3.18",
"@storybook/addon-links": "^5.3.18", "@storybook/addon-links": "^5.3.18",
"@storybook/addon-notes": "^5.3.18", "@storybook/addon-notes": "^5.3.18",
"@storybook/addons": "^5.3.18", "@storybook/addons": "^5.3.18",
@ -148,15 +144,14 @@
"@types/moment-timezone": "^0.5.12", "@types/moment-timezone": "^0.5.12",
"@types/node": "^12.12.30", "@types/node": "^12.12.30",
"@ucap/api": "~0.0.1", "@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-external": "~0.0.2",
"@ucap/api-message": "~0.0.1", "@ucap/api-message": "~0.0.1",
"@ucap/api-prompt": "~0.0.1", "@ucap/api-prompt": "~0.0.1",
"@ucap/api-public": "~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/logger": "~0.0.12",
"@ucap/native": "~0.0.1", "@ucap/native": "~0.0.19",
"@ucap/native-browser": "~0.0.1",
"@ucap/ng-api-common": "file:pack/ucap-ng-api-common-0.0.1.tgz", "@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-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", "@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-core": "file:pack/ucap-ng-core-0.0.7.tgz",
"@ucap/ng-i18n": "file:pack/ucap-ng-i18n-0.0.6.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-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": "file:pack/ucap-ng-native-0.0.5.tgz",
"@ucap/ng-native-browser": "file:pack/ucap-ng-native-browser-0.0.1.tgz",
"@ucap/ng-pi": "file:pack/ucap-ng-pi-0.0.1.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": "file:pack/ucap-ng-protocol-0.0.3.tgz",
"@ucap/ng-protocol-authentication": "file:pack/ucap-ng-protocol-authentication-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-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-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-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-authentication": "file:pack/ucap-ng-store-authentication-0.0.14.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.19.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.14.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.8.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.24.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.25.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.13.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.33.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-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-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-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/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": "~0.0.11",
"@ucap/protocol-authentication": "~0.0.5", "@ucap/protocol-authentication": "~0.0.5",
"@ucap/protocol-buddy": "~0.0.5", "@ucap/protocol-buddy": "~0.0.5",
"@ucap/protocol-event": "~0.0.5", "@ucap/protocol-event": "~0.0.6",
"@ucap/protocol-file": "~0.0.5", "@ucap/protocol-file": "~0.0.6",
"@ucap/protocol-group": "~0.0.5", "@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-inner": "~0.0.4",
"@ucap/protocol-option": "~0.0.7", "@ucap/protocol-option": "~0.0.7",
"@ucap/protocol-ping": "~0.0.4", "@ucap/protocol-ping": "~0.0.4",
"@ucap/protocol-query": "~0.0.5", "@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-service": "~0.0.4",
"@ucap/protocol-status": "~0.0.5", "@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/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-socket": "~0.0.5",
"@ucap/web-storage": "~0.0.5", "@ucap/web-storage": "~0.0.5",
"autolinker": "^3.13.0", "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", "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/native", "dest": "../../dist/native",
"lib": { "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", "name": "@ucap/ng-native",
"version": "0.0.1", "version": "0.0.5",
"publishConfig": { "publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/" "registry": "https://nexus.loafle.net/repository/npm-ucap/"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/common": "^9.0.2", "@angular/common": "^9.0.2",
"@angular/core": "^9.0.2", "@angular/core": "^9.0.2",
"@ucap/core": "~0.0.1",
"@ucap/native": "~0.0.1", "@ucap/native": "~0.0.1",
"axios": "^0.19.2",
"rxjs": "~6.5.4", "rxjs": "~6.5.4",
"tslib": "^1.10.0" "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 * Public API Surface of native
*/ */
export * from './lib/services/notification.service';
export * from './lib/services/browser-native.service';
export * from './lib/types/token'; export * from './lib/types/token';

View File

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

View File

@ -7,7 +7,6 @@ import {
LoginResponse, LoginResponse,
LogoutResponse LogoutResponse
} from '@ucap/protocol-authentication'; } from '@ucap/protocol-authentication';
import { UserResponse, UserRequest } from '@ucap/protocol-info';
/** /**
* request of web login * request of web login
@ -107,29 +106,3 @@ export const sessionDestroyed = createAction(
'[ucap::authentication::login] session Destroyed', '[ucap::authentication::login] session Destroyed',
props<{ error: any }>() 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 { 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 { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects'; import { Actions, ofType, createEffect } from '@ngrx/effects';
import { PiService } from '@ucap/ng-pi';
import { AuthenticationProtocolService } from '@ucap/ng-protocol-authentication'; import { AuthenticationProtocolService } from '@ucap/ng-protocol-authentication';
import { InfoProtocolService } from '@ucap/ng-protocol-info';
import { import { UserActions } from '@ucap/ng-store-organization';
logout,
logoutSuccess, import { logout, logoutSuccess, loginSuccess } from './actions';
infoUser,
infoUserSuccess,
infoUserFailure
} from './actions';
import { UserResponse } from '@ucap/protocol-info';
@Injectable() @Injectable()
export class Effects { export class Effects {
@ -32,27 +26,37 @@ export class Effects {
); );
}); });
infoUser$ = createEffect(() => loginSuccessForOrganizationUserInit$ = createEffect(
() =>
this.actions$.pipe( this.actions$.pipe(
ofType(infoUser), ofType(loginSuccess),
map((action) => action.req), map((params) => params.res),
exhaustMap((req) => tap((loginRes) => {
this.infoProtocolService.user(req).pipe( this.store.dispatch(
map((res: UserResponse) => { UserActions.init({
return infoUserSuccess({ user: {
res info: loginRes.userInfo,
}); companyCode: loginRes.companyCode,
}), departmentCode: loginRes.departmentCode,
catchError((error) => of(infoUserFailure({ error }))) 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( constructor(
private actions$: Actions, private actions$: Actions,
private piService: PiService, private store: Store<any>,
private authenticationProtocolService: AuthenticationProtocolService, private authenticationProtocolService: AuthenticationProtocolService
private infoProtocolService: InfoProtocolService
) {} ) {}
} }

View File

@ -1,8 +1,7 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { initialState } from './state'; import { initialState } from './state';
import { loginSuccess, logoutSuccess, infoUserSuccess } from './actions'; import { loginSuccess, logoutSuccess } from './actions';
import { UserInfoUpdateType } from '@ucap/protocol-info';
export const reducer = createReducer( export const reducer = createReducer(
initialState, initialState,
@ -17,41 +16,5 @@ export const reducer = createReducer(
return { return {
...initialState ...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/store": "@ngrx/store",
"@ngrx/entity": "@ngrx/entity", "@ngrx/entity": "@ngrx/entity",
"@ngrx/effects": "@ngrx/effects", "@ngrx/effects": "@ngrx/effects",
"@ucap/api": "@ucap/api",
"@ucap/pi": "@ucap/pi", "@ucap/pi": "@ucap/pi",
"@ucap/protocol-event": "@ucap/protocol-event", "@ucap/protocol-event": "@ucap/protocol-event",
"@ucap/protocol-file": "@ucap/protocol-file", "@ucap/protocol-file": "@ucap/protocol-file",
"@ucap/protocol-info": "@ucap/protocol-info",
"@ucap/protocol-room": "@ucap/protocol-room", "@ucap/protocol-room": "@ucap/protocol-room",
"@ucap/protocol-sync": "@ucap/protocol-sync", "@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-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-protocol-sync": "@ucap/ng-protocol-sync",
"@ucap/ng-store-organization": "@ucap/ng-store-organization",
"@ucap/ng-store-authentication": "@ucap/ng-store-authentication" "@ucap/ng-store-authentication": "@ucap/ng-store-authentication"
} }
} }

View File

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

View File

@ -1,5 +1,7 @@
import { createAction, props } from '@ngrx/store'; import { createAction, props } from '@ngrx/store';
import { DeviceType } from '@ucap/core';
import { import {
Info, Info,
InfoRequest as EventInfoRequest, InfoRequest as EventInfoRequest,
@ -16,14 +18,17 @@ import {
CancelNotification, CancelNotification,
DelNotification, DelNotification,
EventJson, EventJson,
ReadResponse ReadResponse,
SendRequest
} from '@ucap/protocol-event'; } from '@ucap/protocol-event';
import { import {
InfoRequest as FileInfoRequest, InfoRequest as FileInfoRequest,
InfoResponse as FileInfoResponse, InfoResponse as FileInfoResponse,
FileDownloadInfo, FileDownloadInfo,
FileInfo FileInfo,
DownCheckRequest,
DownCheckResponse
} from '@ucap/protocol-file'; } from '@ucap/protocol-file';
/** /**
@ -51,6 +56,13 @@ export const eventsFailure = createAction(
'[ucap::chat::chatting] events Failure', '[ucap::chat::chatting] events Failure',
props<{ roomId: string; error: any }>() 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 * retrieve list of file information
@ -78,6 +90,19 @@ export const fileInfosFailure = createAction(
props<{ roomId: string; error: any }>() 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 * add new event
*/ */
@ -143,3 +168,109 @@ export const sendNotification = createAction(
'[ucap::chat::chatting] Send Notification', '[ucap::chat::chatting] Send Notification',
props<{ noti: SendNotification }>() 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, exhaustMap,
concatMap, concatMap,
withLatestFrom, withLatestFrom,
debounceTime debounceTime,
mergeMap,
take
} from 'rxjs/operators'; } from 'rxjs/operators';
import { Injectable, Inject } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
@ -16,11 +18,46 @@ import { Store, select } from '@ngrx/store';
import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Dictionary } from '@ngrx/entity'; 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 { EventProtocolService } from '@ucap/ng-protocol-event';
import { RoomProtocolService } from '@ucap/ng-protocol-room'; import { RoomProtocolService } from '@ucap/ng-protocol-room';
import { FileProtocolService } from '@ucap/ng-protocol-file'; 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 * as RoomActions from '../room/actions';
import { import {
events, events,
eventsFailure, eventsFailure,
@ -35,18 +72,26 @@ import {
sendSuccess, sendSuccess,
sendFailure, sendFailure,
addEvent, addEvent,
addEventSuccess addEventSuccess,
del,
delNotification,
delFailure,
delEventList,
moreEvents,
forward,
forwardFailure,
forwardAfterRoomOpen,
roomOpenAfterForward,
forwarForFileEvent,
cancel,
cancelFailure,
cancelNotification,
updateEventList,
intervalClearEvent,
fileDownCheck,
fileDownCheckSuccess,
fileDownCheckFailure
} from './actions'; } 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'; import { Chatting } from './state';
@Injectable() @Injectable()
@ -110,6 +155,40 @@ export class Effects {
{ dispatch: false } { 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( read$ = createEffect(
() => { () => {
return this.actions$.pipe( return this.actions$.pipe(
@ -117,7 +196,14 @@ export class Effects {
exhaustMap((req) => exhaustMap((req) =>
this.eventProtocolService.read(req).pipe( this.eventProtocolService.read(req).pipe(
map((res: ReadResponse) => { map((res: ReadResponse) => {
// room user lastReadEventSeq reset.
this.store.dispatch(readSuccess(res)); this.store.dispatch(readSuccess(res));
// room noReadCount reset.
this.store.dispatch(
RoomActions.updateUnreadCount({
roomId: res.roomId
})
);
}), }),
catchError((error) => of(readFailure({ error }))) catchError((error) => of(readFailure({ error })))
) )
@ -152,6 +238,23 @@ export class Effects {
{ dispatch: false } { 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( addEvent$ = createEffect(
() => { () => {
return this.actions$.pipe( return this.actions$.pipe(
@ -234,6 +337,352 @@ export class Effects {
{ dispatch: false } { 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.] * [Room Action watching.]
*******************************************************************/ *******************************************************************/
@ -263,6 +712,10 @@ export class Effects {
@Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig, @Inject(_MODULE_CONFIG) private moduleConfig: ModuleConfig,
private roomProtocolService: RoomProtocolService, private roomProtocolService: RoomProtocolService,
private eventProtocolService: EventProtocolService, 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 { 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 { import {
initialState, initialState,
adapterChatting, adapterChatting,
adapterEventList, adapterEventList,
Chatting, Chatting,
adapterFileInfoList, adapterFileInfoList
adapterFileInfoCheckList // adapterFileInfoCheckList
} from './state'; } from './state';
import * as RoomActions from '../room/actions';
import { import {
eventsSuccess, eventsSuccess,
eventsFailure, eventsFailure,
fileInfosSuccess, fileInfosSuccess,
fileInfosFailure, fileInfosFailure,
addEvent addEvent,
delEventList,
updateEventList,
clearActiveRoomId
} from './actions'; } from './actions';
export const reducer = createReducer( export const reducer = createReducer(
@ -31,22 +38,53 @@ export const reducer = createReducer(
eventListProcessing: false, eventListProcessing: false,
eventList: adapterEventList.getInitialState(), eventList: adapterEventList.getInitialState(),
eventStatus: null, eventStatus: null,
remainEvent: false, remainEvent: null,
fileInfoListProcessing: false, fileInfoListProcessing: false,
fileInfoList: adapterFileInfoList.getInitialState(), fileInfoList: adapterFileInfoList.getInitialState(),
fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), // fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
fileInfoCheckList: [],
fileInfoSyncDate: '', fileInfoSyncDate: '',
...chatting ...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 = {
...trgtChatting, ...trgtChatting,
eventList: adapterEventList.upsertMany(action.eventInfoList, { eventList: adapterEventList.upsertMany(trgtEventInfoList, {
...trgtChatting.eventList ...trgtChatting.eventList
}), }),
eventStatus: action.res, eventStatus: action.res,
remainEvent: action.remainEvent, remainEvent:
trgtChatting.remainEvent === false ? false : action.remainEvent, // 재조회를 위한 처리.
eventListProcessing: false eventListProcessing: false
}; };
@ -92,11 +130,12 @@ export const reducer = createReducer(
eventListProcessing: false, eventListProcessing: false,
eventList: adapterEventList.getInitialState(), eventList: adapterEventList.getInitialState(),
eventStatus: null, eventStatus: null,
remainEvent: false, remainEvent: null,
fileInfoListProcessing: false, fileInfoListProcessing: false,
fileInfoList: adapterFileInfoList.getInitialState(), fileInfoList: adapterFileInfoList.getInitialState(),
fileInfoCheckList: adapterFileInfoCheckList.getInitialState(), // fileInfoCheckList: adapterFileInfoCheckList.getInitialState(),
fileInfoCheckList: [],
fileInfoSyncDate: '', fileInfoSyncDate: '',
...chatting ...chatting
}; };
@ -111,11 +150,12 @@ export const reducer = createReducer(
...trgtChatting.fileInfoList ...trgtChatting.fileInfoList
}) })
: trgtChatting.fileInfoList, : trgtChatting.fileInfoList,
fileInfoCheckList: !!fileInfoCheckList // fileInfoCheckList: !!fileInfoCheckList
? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, { // ? adapterFileInfoCheckList.upsertMany(fileInfoCheckList, {
...trgtChatting.fileInfoCheckList // ...trgtChatting.fileInfoCheckList
}) // })
: trgtChatting.fileInfoCheckList, // : trgtChatting.fileInfoCheckList,
fileInfoCheckList,
fileInfoListProcessing: false 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.] * [Room Action watching.]
*******************************************************************/ *******************************************************************/
@ -197,7 +348,9 @@ export const reducer = createReducer(
return { return {
...state, ...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 { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity';
import { InfoResponse, Info, EventJson } from '@ucap/protocol-event'; import { InfoResponse, Info, EventJson } from '@ucap/protocol-event';
import { FileInfo, FileDownloadInfo } from '@ucap/protocol-file'; import { FileInfo, FileDownloadInfo } from '@ucap/protocol-file';
export interface EventListState extends EntityState<Info<EventJson>> {} export interface EventListState extends EntityState<Info<EventJson>> {}
export interface FileInfoListState extends EntityState<FileInfo> {} export interface FileInfoListState extends EntityState<FileInfo> {}
export interface FileInfoCheckListState extends EntityState<FileDownloadInfo> {} // export interface FileInfoCheckListState extends EntityState<FileDownloadInfo> {}
export const adapterEventList = createEntityAdapter<Info<EventJson>>({ export const adapterEventList = createEntityAdapter<Info<EventJson>>({
selectId: (info) => info.seq, selectId: (info) => info.seq,
@ -21,12 +20,12 @@ export const adapterFileInfoList = createEntityAdapter<FileInfo>({
return b.seq - a.seq; return b.seq - a.seq;
} }
}); });
export const adapterFileInfoCheckList = createEntityAdapter<FileDownloadInfo>({ // export const adapterFileInfoCheckList = createEntityAdapter<FileDownloadInfo>({
selectId: (info) => info.seq, // selectId: (info) => info.seq,
sortComparer: (a, b) => { // sortComparer: (a, b) => {
return b.seq - a.seq; // return b.seq - a.seq;
} // }
}); // });
const eventListInitialState: EventListState = adapterEventList.getInitialState( const eventListInitialState: EventListState = adapterEventList.getInitialState(
{} {}
@ -34,9 +33,9 @@ const eventListInitialState: EventListState = adapterEventList.getInitialState(
const fileInfoListInitialState: FileInfoListState = adapterFileInfoList.getInitialState( const fileInfoListInitialState: FileInfoListState = adapterFileInfoList.getInitialState(
{} {}
); );
const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState( // const fileInfoCheckListInitialState: FileInfoCheckListState = adapterFileInfoCheckList.getInitialState(
{} // {}
); // );
const { const {
selectAll: selectAllForEventList, selectAll: selectAllForEventList,
@ -52,12 +51,12 @@ const {
selectTotal: selectTotalForFileInfoList selectTotal: selectTotalForFileInfoList
} = adapterFileInfoList.getSelectors(); } = adapterFileInfoList.getSelectors();
const { // const {
selectAll: selectAllForFileInfoCheckList, // selectAll: selectAllForFileInfoCheckList,
selectEntities: selectEntitiesForFileInfoCheckList, // selectEntities: selectEntitiesForFileInfoCheckList,
selectIds: selectIdsForFileInfoCheckList, // selectIds: selectIdsForFileInfoCheckList,
selectTotal: selectTotalForFileInfoCheckList // selectTotal: selectTotalForFileInfoCheckList
} = adapterFileInfoCheckList.getSelectors(); // } = adapterFileInfoCheckList.getSelectors();
export interface Chatting { export interface Chatting {
roomId?: string; roomId?: string;
@ -65,11 +64,12 @@ export interface Chatting {
eventListProcessing?: boolean; eventListProcessing?: boolean;
eventList?: EventListState; eventList?: EventListState;
eventStatus?: InfoResponse | null; eventStatus?: InfoResponse | null;
remainEvent?: boolean; remainEvent?: boolean | null;
fileInfoListProcessing?: boolean; fileInfoListProcessing?: boolean;
fileInfoList?: FileInfoListState; fileInfoList?: FileInfoListState;
fileInfoCheckList?: FileInfoCheckListState; // fileInfoCheckList?: FileInfoCheckListState;
fileInfoCheckList?: FileDownloadInfo[];
fileInfoSyncDate?: string; fileInfoSyncDate?: string;
} }
@ -139,12 +139,37 @@ export function selectors<S>(selector: Selector<any, State>) {
(state) => state.fileInfoListProcessing (state) => state.fileInfoListProcessing
); );
const selectChattingFileInfoList = createSelector( const selectChattingFileInfoList = createSelector(
selectChatting, selectChattings,
(state) => state.fileInfoList (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( const selectChattingFileInfoCheckList = createSelector(
selectChatting, selectChattings,
(state) => state.fileInfoCheckList (state: ChattingState, roomId: string) => {
const chatting = state.entities && state.entities[roomId];
if (!!chatting) {
return chatting?.fileInfoCheckList;
} else {
return [];
}
}
); );
const selectChattingFileInfoSyncDate = createSelector( const selectChattingFileInfoSyncDate = createSelector(
selectChatting, selectChatting,
@ -167,10 +192,11 @@ export function selectors<S>(selector: Selector<any, State>) {
selectChattingFileInfoList, selectChattingFileInfoList,
selectAllForFileInfoList selectAllForFileInfoList
), ),
fileInfoCheckList: createSelector( // fileInfoCheckList: createSelector(
selectChattingFileInfoCheckList, // selectChattingFileInfoCheckList,
selectAllForFileInfoCheckList // selectAllForFileInfoCheckList
), // ),
fileInfoCheckList: selectChattingFileInfoCheckList,
fileInfoSyncDate: selectChattingFileInfoSyncDate fileInfoSyncDate: selectChattingFileInfoSyncDate
}; };
} }

View File

@ -160,21 +160,6 @@ export const create = createAction(
'[ucap::chat::room] create', '[ucap::chat::room] create',
props<{ req: CreateRequest }>() 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 * create timer room
*/ */
@ -183,17 +168,17 @@ export const createTimer = createAction(
props<{ req: CreateTimerRequest }>() props<{ req: CreateTimerRequest }>()
); );
/** /**
* Success of openTimer request * Success of create room / timer room request
*/ */
export const createTimerSuccess = createAction( export const createRoomSuccess = createAction(
'[ucap::chat::room] createTimer Success', '[ucap::chat::room] create room Success',
props<{ res: CreateTimerResponse }>() props<{ res: CreateResponse; isForward?: boolean }>()
); );
/** /**
* Failure of createTimer request * Failure of create room / timer room request
*/ */
export const createTimerFailure = createAction( export const createRoomFailure = createAction(
'[ucap::chat::room] createTimer Failure', '[ucap::chat::room] create room Failure',
props<{ error: any }>() props<{ error: any }>()
); );
@ -264,6 +249,15 @@ export const updateFailure = createAction(
'[ucap::chat::room] update Failure', '[ucap::chat::room] update Failure',
props<{ error: any }>() 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 * update isJoinRoom of user information from true to false
@ -331,18 +325,6 @@ export const closeFailure = createAction(
props<{ error: any }>() 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 * Invite conversation partner to room
*/ */
@ -386,6 +368,13 @@ export const expelFailure = createAction(
'[ucap::chat::room] expel Failure', '[ucap::chat::room] expel Failure',
props<{ error: any }>() props<{ error: any }>()
); );
/**
* expel notification
*/
export const expelNotification = createAction(
'[ucap::chat::room] expel Notification',
props<{ res: ExitForcingResponse }>()
);
/** /**
* update interval of timer room * update interval of timer room
@ -422,10 +411,24 @@ export const inviteNotification = createAction(
*/ */
export const exitNotification = createAction( export const exitNotification = createAction(
'[ucap::chat::room] Exit Notification', '[ucap::chat::room] Exit Notification',
props<{ roomId: string; userSeq: string; senderSeq: string }>() props<{ roomId: string; senderSeq: string }>()
); );
export const updateUnreadCount = createAction( export const updateUnreadCount = createAction(
'[ucap::chat::room] Update unread count', '[ucap::chat::room] Update unread count',
props<{ roomId: string; noReadCnt?: number }>() 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 { Store, select } from '@ngrx/store';
import { Actions, ofType, createEffect } from '@ngrx/effects'; import { Actions, ofType, createEffect } from '@ngrx/effects';
import { LocaleCode } from '@ucap/core';
import { import {
RoomType,
OpenResponse as CreateResponse, OpenResponse as CreateResponse,
Open3Response as CreateTimerResponse, Open3Response as CreateTimerResponse,
ExitResponse as DeleteResponse, ExitResponse as DeleteResponse,
@ -24,22 +24,30 @@ import {
InviteResponse, InviteResponse,
ExitForcingResponse, ExitForcingResponse,
UpdateTimerSetResponse, UpdateTimerSetResponse,
InfoRequest InfoRequest,
UserInfoShort,
UserInfo,
RoomInfo
} from '@ucap/protocol-room'; } 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 { RoomProtocolService } from '@ucap/ng-protocol-room';
import { SyncProtocolService } from '@ucap/ng-protocol-sync'; import { SyncProtocolService } from '@ucap/ng-protocol-sync';
import {
PresenceActions,
CommonActions,
UserSelector
} from '@ucap/ng-store-organization';
import { LoginActions } from '@ucap/ng-store-authentication'; 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 { import {
rooms, rooms,
roomsFailure,
roomsSuccess,
room, room,
roomSuccess,
roomFailure, roomFailure,
inviteNotification, inviteNotification,
exitNotification, exitNotification,
@ -48,11 +56,9 @@ import {
close, close,
delSuccess, delSuccess,
create, create,
createSuccess,
createFailure,
createTimer, createTimer,
createTimerSuccess, createRoomSuccess,
createTimerFailure, createRoomFailure,
del, del,
delFailure, delFailure,
update, update,
@ -61,7 +67,6 @@ import {
open, open,
openSuccess, openSuccess,
closeSuccess, closeSuccess,
inviteOrCreate,
invite, invite,
inviteSuccess, inviteSuccess,
inviteFailure, inviteFailure,
@ -79,10 +84,9 @@ import {
selectedRoom, selectedRoom,
room2Success, room2Success,
selectedRoomSuccess, selectedRoomSuccess,
clearSelectedRoom clearSelectedRoom,
expelNotification
} from './actions'; } from './actions';
import { Router } from '@angular/router';
import { LocaleCode } from '@ucap/core';
@Injectable() @Injectable()
export class Effects { export class Effects {
@ -121,14 +125,25 @@ export class Effects {
roomInfo2Res: res 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 { } else {
// is not join room. so, redirect chat main. // is not join room. so, redirect chat main.
this.router.navigate([
'chat',
{
outlets: { content: 'index' }
}
]);
this.store.dispatch( this.store.dispatch(
clearSelectedRoom({ roomId: req.roomId }) clearSelectedRoom({ roomId: req.roomId })
); );
@ -237,27 +252,9 @@ export class Effects {
exhaustMap((req) => { exhaustMap((req) => {
return this.roomProtocolService.open(req).pipe( return this.roomProtocolService.open(req).pipe(
map((res: CreateResponse) => { map((res: CreateResponse) => {
console.log('CreateResponse', res); this.store.dispatch(createRoomSuccess({ res }));
this.store.dispatch(createSuccess({ res }));
this.router.navigate(
[
'chat',
{
outlets: { content: 'chatroom' }
}
],
{
queryParams: { roomId: res.roomId }
}
);
// if (!res.newRoom) {
// } else {
// }
}), }),
catchError((error) => of(createFailure({ error }))) catchError((error) => of(createRoomFailure({ error })))
); );
}) })
); );
@ -265,33 +262,53 @@ export class Effects {
{ dispatch: false } { dispatch: false }
); );
createTimer$ = createEffect(() => createTimer$ = createEffect(
this.actions$.pipe( () => {
return this.actions$.pipe(
ofType(createTimer), ofType(createTimer),
map((action) => action.req), map((action) => action.req),
exhaustMap((req) => { exhaustMap((req) => {
return this.roomProtocolService.open3(req).pipe( return this.roomProtocolService.open3(req).pipe(
map((res: CreateTimerResponse) => { 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(() => del$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(del), ofType(del),
map((action) => action.req), 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( 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 room, clear chatting
close({ roomIds: [res.roomId] }), close({ roomIds: [res.roomId] }),
// clear room in rooms. // clear room in rooms.
delSuccess({ res }) delSuccess({ res })
]), ];
} else {
return [
// close room, clear chatting
close({ roomIds: [res.roomId] }),
// clear room in rooms.
delSuccess({ res })
];
}
}),
catchError((error) => of(delFailure({ error }))) catchError((error) => of(delFailure({ error })))
); );
}) })
@ -302,14 +319,32 @@ export class Effects {
this.actions$.pipe( this.actions$.pipe(
ofType(delMulti), ofType(delMulti),
map((action) => action.req), 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( 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 room, clear chatting
close({ roomIds: res.roomIds }), close({ roomIds: res.roomIds }),
// clear room in rooms. // clear room in rooms.
delMultiSuccess({ res }) delMultiSuccess({ res })
]), ];
} else {
return [
// close room, clear chatting
close({ roomIds: res.roomIds }),
// clear room in rooms.
delMultiSuccess({ res })
];
}
}),
catchError((error) => of(delMultiFailure({ error }))) 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(() => invite$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(invite), ofType(invite),
@ -398,7 +406,7 @@ export class Effects {
room({ room({
req: { req: {
roomId: req.roomId, roomId: req.roomId,
isDetail: true, isDetail: false,
localeCode 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(() => updateTimeRoomInterval$ = createEffect(() =>
this.actions$.pipe( this.actions$.pipe(
ofType(updateTimeRoomInterval), 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( return this.actions$.pipe(
ofType(inviteNotification), 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({ room({
req: { req: {
roomId: action.noti.roomId, roomId,
isDetail: true, isDetail: false,
localeCode: action.localeCode localeCode: action.localeCode
} }
}) })
)
); );
}); }
}
})
);
},
{
dispatch: false
}
);
exitNotification$ = createEffect(() => { exitNotification$ = createEffect(() => {
return this.actions$.pipe( return this.actions$.pipe(
ofType(exitNotification), ofType(exitNotification),
switchMap((action) => { withLatestFrom(this.store.pipe(select(UserSelector.user))),
if (action.userSeq === action.senderSeq) { switchMap(([action, user]) => {
if (String(user.info.seq) === String(action.senderSeq)) {
return [ return [
close({ roomIds: [action.roomId] }), close({ roomIds: [action.roomId] }),
clearSelectedRoom({ roomId: action.roomId }),
delSuccess({ delSuccess({
res: { roomId: action.roomId } res: { roomId: action.roomId }
}) })
@ -490,7 +559,134 @@ export class Effects {
const roomId = action.roomId; const roomId = action.roomId;
if (!roomList.find((roomInfo) => roomInfo.roomId === 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( constructor(
private actions$: Actions, private actions$: Actions,
private store: Store<any>, private store: Store<any>,
private router: Router,
private syncProtocolService: SyncProtocolService, 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 { createReducer, on } from '@ngrx/store';
import { import { RoomInfo } from '@ucap/protocol-room';
UserInfo as RoomUserInfo, import { Info, EventJson } from '@ucap/protocol-event';
UserInfoShort as RoomUserInfoShort,
RoomInfo
} from '@ucap/protocol-room';
import * as chattingActions from '../chatting/actions'; import * as ChattingActions from '../chatting/actions';
import { ChatUtil } from '../../utils/chat.util';
import { import {
initialState, initialState,
@ -19,18 +17,17 @@ import {
import { import {
roomsSuccess, roomsSuccess,
roomSuccess, roomSuccess,
excludeUser,
excludeUserSuccess, excludeUserSuccess,
delSuccess, delSuccess,
rooms2Success, rooms2Success,
delMultiSuccess, delMultiSuccess,
updateSuccess, updateSuccess,
room2Success, room2Success,
createSuccess, createRoomSuccess,
updateUnreadCount updateUnreadCount,
updateRoomName,
updateTimeRoomIntervalSuccess
} from './actions'; } from './actions';
import { Info, EventJson } from '@ucap/protocol-event';
import { ChatUtil } from '../../utils/chat.util';
export const reducer = createReducer( export const reducer = createReducer(
initialState, initialState,
@ -184,63 +181,88 @@ export const reducer = createReducer(
on(excludeUserSuccess, (state, action) => { on(excludeUserSuccess, (state, action) => {
const roomId = action.roomId; 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 { return {
...state, ...roomUserInfo,
roomUsers: adapterRoomUser.updateOne( isJoinRoom: false
{ };
id: roomId, } else {
changes: { return roomUserInfo;
roomId,
userInfos
} }
},
{
...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 standby = state.standbyRooms;
const curRoomId = action.res.roomId; 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. // Exist.
return state; return state;
} else { } 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) => { on(updateSuccess, (state, action) => {
const roomInfo = { const roomInfo = {
...state.rooms.entities[action.res.roomId], ...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) => { on(delSuccess, (state, action) => {
const roomId = action.res.roomId; const roomId = action.res.roomId;
const room = state.rooms.entities[roomId]; const room = state.rooms.entities[roomId];
@ -353,7 +379,7 @@ export const reducer = createReducer(
/******************************************************************* /*******************************************************************
* [Chatting Action watching.] * [Chatting Action watching.]
*******************************************************************/ *******************************************************************/
on(chattingActions.readSuccess, (state, action) => { on(ChattingActions.readSuccess, (state, action) => {
const roomId = action.roomId; const roomId = action.roomId;
const trgtUserSeq = action.SENDER_SEQ; 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 roomId = action.roomId;
const info: Info<EventJson> = action.info; const info: Info<EventJson> = action.info;
@ -434,6 +460,12 @@ export const reducer = createReducer(
action.info.sentMessageJson || action.info.sentMessage action.info.sentMessageJson || action.info.sentMessage
); );
if (!finalEventMessage) {
/**
* .
*/
return state;
} else {
const roomInfo = { const roomInfo = {
...currentRoomInfo, ...currentRoomInfo,
finalEventType: info.type, finalEventType: info.type,
@ -447,6 +479,7 @@ export const reducer = createReducer(
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }), rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }),
standbyRooms: fixedStandByRooms standbyRooms: fixedStandByRooms
}; };
}
} else { } else {
return state; return state;
} }

View File

@ -1,9 +1,8 @@
import moment from 'moment'; import moment from 'moment';
import { Selector, createSelector } from '@ngrx/store'; 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 { import {
RoomInfo, RoomInfo,
UserInfo as RoomUserInfo, UserInfo as RoomUserInfo,
@ -134,16 +133,23 @@ export function selectors<S>(selector: Selector<any, State>) {
selector, selector,
(state: State) => state.standbyRooms (state: State) => state.standbyRooms
), ),
unreadTotal: createSelector( unreadTotal: createSelector(selectRooms, (roomState: RoomState) => {
selectRooms,
selectAllForRoom,
(roomState: RoomState, rooms: RoomInfo[]) => {
let unreadTotal = 0; 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; unreadTotal += room.noReadCnt;
} }
return unreadTotal;
} }
)
return unreadTotal;
})
}; };
} }

View File

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

View File

@ -10,6 +10,9 @@ export * from './lib/config/module-config';
export { CommonActions, RoomActions, ChattingActions }; export { CommonActions, RoomActions, ChattingActions };
export * from './lib/utils/chat.util';
export * from './lib/store/state'; 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'; export * from './lib/chat-store.module';

View File

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

View File

@ -1,6 +1,6 @@
import { createAction, props } from '@ngrx/store'; import { createAction, props } from '@ngrx/store';
import { UserInfo } from '@ucap/protocol-sync';
import { UserInfo } from '@ucap/protocol-sync';
import { import {
AddRequest as BuddyAddRequest, AddRequest as BuddyAddRequest,
AddResponse as BuddyAddResponse, AddResponse as BuddyAddResponse,
@ -11,7 +11,8 @@ import {
} from '@ucap/protocol-buddy'; } from '@ucap/protocol-buddy';
import { import {
UserNicknameRequest as NicknameRequest, UserNicknameRequest as NicknameRequest,
UserNicknameResponse as NicknameResponse UserNicknameResponse as NicknameResponse,
UserNotification
} from '@ucap/protocol-info'; } from '@ucap/protocol-info';
/** /**
@ -142,3 +143,10 @@ export const nicknameFailure = createAction(
'[ucap::group::buddy] user nickname Failure', '[ucap::group::buddy] user nickname Failure',
props<{ error: any }>() 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 { import {
DepartmentSelector, DepartmentSelector,
PresenceActions PresenceActions,
CommonActions,
UserSelector
} from '@ucap/ng-store-organization'; } from '@ucap/ng-store-organization';
import { LoginActions } from '@ucap/ng-store-authentication'; import { LoginActions } from '@ucap/ng-store-authentication';
import * as groupActions from '../group/actions'; import * as groupActions from '../group/actions';
@ -51,7 +54,8 @@ import {
nickname, nickname,
nicknameSuccess, nicknameSuccess,
nicknameFailure, nicknameFailure,
delAndClear delAndClear,
buddyInfoUpdate
} from './actions'; } from './actions';
import { BuddySelector, GroupSelector } from '../state'; import { BuddySelector, GroupSelector } from '../state';
@ -135,7 +139,7 @@ export class Effects {
), ),
tap(([req, groupList, myDeptUserList]) => { tap(([req, groupList, myDeptUserList]) => {
for (const group of groupList) { for (const group of groupList) {
if (group.userSeqs.indexOf(req.seq as any) > -1) { if (group.userSeqs.indexOf(String(req.seq)) > -1) {
// 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다. // 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다.
if ( if (
!!this.moduleConfig.useMyDeptGroup && !!this.moduleConfig.useMyDeptGroup &&
@ -154,7 +158,7 @@ export class Effects {
groupSeq: group.seq, groupSeq: group.seq,
groupName: group.name, groupName: group.name,
userSeqs: group.userSeqs.filter( 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 && this.moduleConfig.useMyDeptGroup &&
!!myDeptUserList && !!myDeptUserList &&
myDeptUserList.filter( myDeptUserList.filter(
(deptUser) => deptUser.seq === (req.seq as any) (deptUser) => deptUser.seq === String(req.seq)
).length > 0 ).length > 0
) { ) {
// skip;; // skip;;
@ -268,25 +272,42 @@ export class Effects {
const userSeqsForDelete = action.userSeqsForDelete; const userSeqsForDelete = action.userSeqsForDelete;
if (!!targetUserSeqs && 0 < targetUserSeqs.length) { if (!!targetUserSeqs && 0 < targetUserSeqs.length) {
const addBuddyList: string[] = []; // Add Buddy
targetUserSeqs.forEach((userSeq) => { const userSeqsForAdd: string[] = [];
if (!buddyList) {
addBuddyList.push(userSeq);
return;
}
const index = buddyList.findIndex( targetUserSeqs.map((seq) => {
(b) => b.seq === Number(userSeq) const findBuddy = buddyList.filter(
(user) => Number(user.seq) === Number(seq)
); );
if (-1 < index) {
addBuddyList.push(userSeq); if (!!findBuddy && findBuddy.length === 0) {
return; userSeqsForAdd.push(seq);
} }
}); });
if (addBuddyList.length > 0) { if (userSeqsForAdd.length > 0) {
this.store.dispatch(add({ req: { userSeqs: addBuddyList } })); 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) { if (!!userSeqsForDelete && 0 < userSeqsForDelete.length) {
@ -304,6 +325,21 @@ export class Effects {
{ dispatch: false } { 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( constructor(
private actions$: Actions, private actions$: Actions,
private store: Store<any>, private store: Store<any>,

View File

@ -1,5 +1,6 @@
import { createReducer, on } from '@ngrx/store'; import { createReducer, on } from '@ngrx/store';
import { UserInfoUpdateType } from '@ucap/protocol-info';
import { UserInfo } from '@ucap/protocol-sync'; import { UserInfo } from '@ucap/protocol-sync';
import { initialState, adapterBuddy } from './state'; import { initialState, adapterBuddy } from './state';
@ -7,7 +8,8 @@ import {
buddy2Success, buddy2Success,
delSuccess, delSuccess,
updateSuccess, updateSuccess,
nicknameSuccess nicknameSuccess,
buddyInfoUpdate
} from './actions'; } from './actions';
export const reducer = createReducer( export const reducer = createReducer(
@ -56,5 +58,49 @@ export const reducer = createReducer(
...state, ...state,
buddies: adapterBuddy.upsertOne(userInfo, { ...state.buddies }) 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 { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter } from '@ngrx/entity'; import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { UserInfo } from '@ucap/protocol-sync'; import { UserInfo } from '@ucap/protocol-sync';
export interface BuddyState extends EntityState<UserInfo> { export interface BuddyState extends EntityState<UserInfo> {
syncDate: string; syncDate: string;
} }
export const adapterBuddy = createEntityAdapter<UserInfo>({ export const adapterBuddy = createEntityAdapter<UserInfo>({
selectId: userInfo => userInfo.seq selectId: (userInfo) => userInfo.seq
}); });
export interface State { export interface State {
@ -38,7 +39,7 @@ export function selectors<S>(selector: Selector<any, State>) {
buddies: createSelector(selectBuddies, selectAllForBuddy), buddies: createSelector(selectBuddies, selectAllForBuddy),
buddySyncDate: createSelector( buddySyncDate: createSelector(
selectBuddies, selectBuddies,
buddyState => buddyState.syncDate (buddyState) => buddyState.syncDate
) )
}; };
} }

View File

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

View File

@ -11,9 +11,9 @@
"@ucap/protocol-query": "@ucap/protocol-query", "@ucap/protocol-query": "@ucap/protocol-query",
"@ucap/protocol-status": "@ucap/protocol-status", "@ucap/protocol-status": "@ucap/protocol-status",
"@ucap/ng-api-external": "@ucap/ng-api-external", "@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-query": "@ucap/ng-protocol-query",
"@ucap/ng-protocol-status": "@ucap/ng-protocol-status", "@ucap/ng-protocol-status": "@ucap/ng-protocol-status"
"@ucap/ng-store-authentication": "@ucap/ng-store-authentication"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@ucap/ng-store-organization", "name": "@ucap/ng-store-organization",
"version": "0.0.8", "version": "0.0.20",
"publishConfig": { "publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/" "registry": "https://nexus.loafle.net/repository/npm-ucap/"
}, },
@ -10,8 +10,8 @@
"@ucap/core": "~0.0.1", "@ucap/core": "~0.0.1",
"@ucap/protocol-query": "~0.0.1", "@ucap/protocol-query": "~0.0.1",
"@ucap/ng-api-external": "~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-protocol-query": "~0.0.1",
"@ucap/ng-store-authentication": "~0.0.1",
"tslib": "^1.10.0" "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 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 { 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() @Injectable()
export class Effects { 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 { Selector, createSelector } from '@ngrx/store';
import { DeptInfo, UserInfoSS } from '@ucap/protocol-query';
import { Company } from '@ucap/api-external'; import { Company } from '@ucap/api-external';
export interface State { export interface State {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import * as CommonState from './common/state';
import * as CompanyState from './company/state'; import * as CompanyState from './company/state';
import * as DepartmentState from './department/state'; import * as DepartmentState from './department/state';
import * as PresenceState from './presence/state'; import * as PresenceState from './presence/state';
import * as UserState from './user/state';
export const KEY_FEATURE = 'organization'; export const KEY_FEATURE = 'organization';
@ -12,6 +13,7 @@ export interface State {
company: CompanyState.State; company: CompanyState.State;
department: DepartmentState.State; department: DepartmentState.State;
presence: PresenceState.State; presence: PresenceState.State;
user: UserState.State;
} }
export const Selector = createFeatureSelector<State>(KEY_FEATURE); export const Selector = createFeatureSelector<State>(KEY_FEATURE);
@ -31,3 +33,7 @@ export const DepartmentSelector = DepartmentState.selectors(
export const PresenceSelector = PresenceState.selectors( export const PresenceSelector = PresenceState.selectors(
createSelector(Selector, (state: State) => state.presence) 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 CompanyActions from './lib/store/company/actions';
import * as DepartmentActions from './lib/store/department/actions'; import * as DepartmentActions from './lib/store/department/actions';
import * as PresenceActions from './lib/store/presence/actions'; import * as PresenceActions from './lib/store/presence/actions';
import * as UserActions from './lib/store/user/actions';
export * from './lib/config/module-config'; export * from './lib/config/module-config';
export { CommonActions, CompanyActions, DepartmentActions, PresenceActions }; export {
CommonActions,
CompanyActions,
DepartmentActions,
PresenceActions,
UserActions
};
export * from './lib/store/state'; export * from './lib/store/state';

View File

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

View File

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

View File

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

View File

@ -2,6 +2,10 @@
* Public API Surface of ui-authentication * 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/module-config';
export * from './lib/config/token';
export * from './lib/authentication-ui.module'; export * from './lib/authentication-ui.module';

View File

@ -5,12 +5,16 @@
"entryFile": "src/public-api.ts", "entryFile": "src/public-api.ts",
"styleIncludePaths": ["./src/assets/scss"], "styleIncludePaths": ["./src/assets/scss"],
"umdModuleIds": { "umdModuleIds": {
"moment": "moment",
"ngx-perfect-scrollbar": "ngx-perfect-scrollbar", "ngx-perfect-scrollbar": "ngx-perfect-scrollbar",
"@ucap/core": "@ucap/core", "@ucap/core": "@ucap/core",
"@ucap/api": "@ucap/api",
"@ucap/protocol-event": "@ucap/protocol-event",
"@ucap/protocol-room": "@ucap/protocol-room", "@ucap/protocol-room": "@ucap/protocol-room",
"@ucap/ng-logger": "@ucap/ng-logger", "@ucap/ng-logger": "@ucap/ng-logger",
"@ucap/ng-i18n": "@ucap/ng-i18n", "@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", "name": "@ucap/ng-ui-chat",
"version": "0.0.13", "version": "0.0.72",
"publishConfig": { "publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/" "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 { FlexLayoutModule } from '@angular/flex-layout';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
import { MatTreeModule } from '@angular/material/tree'; 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 { MatMenuModule } from '@angular/material/menu';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { UiModule } from '@ucap/ng-ui'; import { UiModule } from '@ucap/ng-ui';
import { OrganizationUiModule } from '@ucap/ng-ui-organization';
import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n'; import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { ModuleConfig } from './config/module-config'; import { ModuleConfig } from './config/module-config';
import { _MODULE_CONFIG } from './config/token'; 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 { import {
RoomExpansionComponent, RoomExpansionComponent,
RoomExpansionHeaderDirective, RoomExpansionHeaderDirective,
@ -45,10 +56,19 @@ import { ReadHereComponent } from './components/message-box/read-here.component'
import { DateSplitterComponent } from './components/message-box/date-splitter.component'; import { DateSplitterComponent } from './components/message-box/date-splitter.component';
import { SmsComponent } from './components/message-box/sms.component'; import { SmsComponent } from './components/message-box/sms.component';
import { ReplyComponent } from './components/message-box/reply.component'; import { ReplyComponent } from './components/message-box/reply.component';
import {
ChatItemListComponent,
ChatItemListNodeDirective
} from './components/item-list.component';
const COMPONENTS = [ const COMPONENTS = [
ImageListItem01Component,
FileListItem01Component,
FileListComponent,
ImageListComponent,
RoomExpansionComponent, RoomExpansionComponent,
RoomListItem01Component, RoomListItem01Component,
ChatItemListComponent,
InformationComponent, InformationComponent,
TextComponent, TextComponent,
@ -72,7 +92,13 @@ const COMPONENTS = [
]; ];
const DIALOGS = []; const DIALOGS = [];
const PIPES = []; const PIPES = [];
const DIRECTIVES = [RoomExpansionHeaderDirective, RoomExpansionNodeDirective]; const DIRECTIVES = [
RoomExpansionHeaderDirective,
RoomExpansionNodeDirective,
FileListNodeDirective,
ImageListNodeDirective,
ChatItemListNodeDirective
];
const SERVICES = []; const SERVICES = [];
@NgModule({ @NgModule({
@ -86,7 +112,6 @@ export class ChatUiRootModule {}
imports: [ imports: [
CommonModule, CommonModule,
FlexLayoutModule, FlexLayoutModule,
ScrollingModule,
MatBadgeModule, MatBadgeModule,
MatButtonModule, MatButtonModule,
@ -95,10 +120,13 @@ export class ChatUiRootModule {}
MatRippleModule, MatRippleModule,
MatTreeModule, MatTreeModule,
MatMenuModule, MatMenuModule,
MatProgressBarModule,
MatTooltipModule,
PerfectScrollbarModule, PerfectScrollbarModule,
I18nModule, I18nModule,
OrganizationUiModule,
UiModule UiModule
], ],
exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES], 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'; import { ProfileListItemComponent } from './profile-list-item.component';
describe('ucap::ui-organization::ProfileListItemComponent', () => { describe('ucap::ucap::organization::ProfileListItemComponent', () => {
let component: ProfileListItemComponent; let component: ProfileListItemComponent;
let fixture: ComponentFixture<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도 추가--> <!--파일명에 따라 doc exe hwp ppt xls zip 으로 추가되고 나머지 파일 명은 file로 기간이 만료된 파일은 그뒤에 disable도 추가-->
<!-- <div class="file-img" [ngClass]="fileInfo.FileExt"></div> --> <!-- <div class="file-img" [ngClass]="fileInfo.FileExt"></div> -->
<div [ngClass]="['mime-icon', 'light', 'ico-' + fileInfo.fileExt]"> <div [ngClass]="['mime-icon', 'light', 'ico-' + fileInfo.fileExt]">
@ -10,28 +16,113 @@
</li> </li>
<li class="date"> <li class="date">
{{ fileInfo.attachmentRegDate | ucapDate }} {{ fileInfo.attachmentRegDate | ucapDate }}
<span *ngIf="!!fileRetentionPeriod && fileRetentionPeriod > 0">
~
{{
fileInfo.attachmentRegDate
| ucapDate: 'YYYY.MM.DD':fileRetentionPeriodOptions
}}
</span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-box over"> <div class="btn-box" [ngClass]="!!showOverButtons ? 'over' : ''">
<ul *ngIf="!expired"> <ul>
<li> <li *ngIf="!existFile">
<button mat-button (click)="onClickSave()"> <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> </button>
</li> </li>
<li> <li>
<button mat-button (click)="onClickSave()"> <button
폴더열기 mat-flat-button
</button> color="accent"
</li> class="btn-file-ctrl"
<li> (click)="onClickOpenViewer()"
<button mat-button (click)="onClickSave()"> aria-label="뷰어 열기"
??? [matTooltip]="'label.openViewer' | ucapI18n"
>
<mat-icon>open_in_new</mat-icon>
</button> </button>
</li> </li>
</ul> </ul>
</div> </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> </div>

View File

@ -1,126 +1,124 @@
// $tablet-s-width: 768px; $tablet-s-width: 768px;
// .bubble-main { .ucap-chat-message-box-attach-file-bubble {
// display: flex; display: flex;
// flex-direction: row; flex-direction: row;
// padding: 14px; align-items: center;
// min-width: 300px; //padding: 6px 12px 0 0;
// .file-img { //border-radius: 2px;
// display: inline-flex; cursor: pointer;
// width: 50px; .file-info-box {
// height: 50px; display: flex;
// float: left; flex-direction: row;
// 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;
//align-items: center; //align-items: center;
// border-right: 1px solid #dddddd; //padding: 6px 12px 0 0;
// @media screen and (max-width: #{$tablet-s-width}) { //border-radius: 2px;
// width: 30%; cursor: pointer;
// } .file-info {
// &:last-child { //border-radius: 2px;
// border-right: none; .file-name {
// @media screen and (max-width: #{$tablet-s-width}) { font-weight: 600;
// width: 70%; //line-height: 1.2;
// } //font-size: 0.929em;
// } }
// .mat-button { .date {
// width: 100%; //color: #c92b5c;
// display: block; //font-size: 0.875em;
// height: 100%; //margin-top: 4px;
// font-size: 1em; }
// } }
// } }
// &.expired { .btn-box {
// li { width: 100%;
// width: 100%; height: 100%;
// white-space: nowrap; //background-color: rgba(0, 0, 0, 0.6);
// color: #999999; display: none;
// align-items: center; //transition: all 0.2s linear;
// line-height: 40px; //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 { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FileEventJson } from '@ucap/protocol-event'; import { FileEventJson } from '@ucap/protocol-event';
import { DateOptions } from '@ucap/ng-ui';
import { FileDownloadItem } from '@ucap/api';
import { DeviceType } from '@ucap/core';
@Component({ @Component({
selector: 'ucap-chat-message-box-attach-file', selector: 'ucap-chat-message-box-attach-file',
@ -10,26 +13,83 @@ export class AttachFileComponent implements OnInit {
@Input() @Input()
fileInfo: FileEventJson; fileInfo: FileEventJson;
@Input()
fileDownloadItem: FileDownloadItem;
@Input()
existFile = false;
@Input()
deviceType: DeviceType;
@Input() @Input()
expired = false; expired = false;
@Input()
fileRetentionPeriod?: number;
@Output() @Output()
save = new EventEmitter<string>(); save = new EventEmitter<string>();
@Output()
openFile = new EventEmitter();
@Output()
openFolder = new EventEmitter();
@Output() @Output()
openViewer = new EventEmitter(); openViewer = new EventEmitter();
showOverButtons = false;
DeviceType = DeviceType;
constructor() {} constructor() {}
ngOnInit() {} ngOnInit() {}
onClickSave() { onClickSave() {
if (!this.expired) {
this.save.emit('save'); this.save.emit('save');
} }
}
onClickSaveAs() { onClickSaveAs() {
if (!this.expired) {
this.save.emit('saveAs'); this.save.emit('saveAs');
} }
}
onClickOpenFile() {
if (!this.expired) {
this.openFile.emit();
}
}
onClickOpenFolder() {
if (!this.expired) {
this.openFolder.emit();
}
}
onClickOpenViewer() { onClickOpenViewer() {
if (!this.expired) {
this.openViewer.emit(); 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 <ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.File" *ngSwitchCase="FileType.File"
[fileInfo]="fileInfo" [fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()" [expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)" (openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)" (save)="onSave($event)"
> >
</ucap-chat-message-box-attach-file> </ucap-chat-message-box-attach-file>
@ -15,8 +21,14 @@
<ucap-chat-message-box-attach-file <ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.Sound" *ngSwitchCase="FileType.Sound"
[fileInfo]="fileInfo" [fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()" [expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)" (openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)" (save)="onSave($event)"
> >
</ucap-chat-message-box-attach-file> </ucap-chat-message-box-attach-file>
@ -25,6 +37,7 @@
*ngSwitchCase="FileType.Image" *ngSwitchCase="FileType.Image"
[fileInfo]="fileInfo" [fileInfo]="fileInfo"
[expired]="getExpiredFile()" [expired]="getExpiredFile()"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)" (openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)" (save)="onSave($event)"
></ucap-chat-message-box-image> ></ucap-chat-message-box-image>
@ -32,8 +45,14 @@
<ucap-chat-message-box-video <ucap-chat-message-box-video
*ngSwitchCase="FileType.Video" *ngSwitchCase="FileType.Video"
[fileInfo]="fileInfo" [fileInfo]="fileInfo"
[fileDownloadItem]="fileDownloadItem"
[expired]="getExpiredFile()" [expired]="getExpiredFile()"
[deviceType]="deviceType"
[existFile]="existFile"
[fileRetentionPeriod]="fileRetentionPeriod"
(openViewer)="onClickFileViewer(fileInfo)" (openViewer)="onClickFileViewer(fileInfo)"
(openFile)="onClickOpenFile(fileInfo)"
(openFolder)="onClickOpenFolder(fileInfo)"
(save)="onSave($event)" (save)="onSave($event)"
></ucap-chat-message-box-video> ></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 { FileType, Info, FileEventJson } from '@ucap/protocol-event';
import { RoomInfo } from '@ucap/protocol-room'; import { RoomInfo } from '@ucap/protocol-room';
import { FileDownloadItem, StatusCode } from '@ucap/api'; import { FileDownloadItem, StatusCode } from '@ucap/api';
import { SelectFileInfo } from '@ucap/ng-ui';
import { DeviceType } from '@ucap/core';
@Component({ @Component({
selector: 'ucap-chat-message-box-file', selector: 'ucap-chat-message-box-file',
@ -15,14 +17,30 @@ export class FileComponent implements OnInit {
@Input() @Input()
roomInfo: RoomInfo; roomInfo: RoomInfo;
@Input()
existFile = false;
@Input()
deviceType?: DeviceType;
@Input()
fileRetentionPeriod?: number;
@Output() @Output()
save = new EventEmitter<{ save = new EventEmitter<{
fileInfo: FileEventJson; fileInfo: FileEventJson;
fileDownloadItem: FileDownloadItem; fileDownloadItem: FileDownloadItem;
type: string; type: string;
}>(); }>();
@Output() @Output()
fileViewer = new EventEmitter<FileEventJson>(); fileViewer = new EventEmitter<SelectFileInfo>();
@Output()
openFile = new EventEmitter<FileEventJson>();
@Output()
openFolder = new EventEmitter<FileEventJson>();
fileInfo?: FileEventJson; fileInfo?: FileEventJson;
fileDownloadItem: FileDownloadItem; fileDownloadItem: FileDownloadItem;
@ -55,7 +73,7 @@ export class FileComponent implements OnInit {
onClickFileViewer(fileInfo: FileEventJson) { onClickFileViewer(fileInfo: FileEventJson) {
if (!this.getExpiredFile()) { 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