@nu-art/ts-focused-object-frontend 0.400.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/Component_FocusedEntityRef.d.ts +32 -0
- package/components/Component_FocusedEntityRef.js +57 -0
- package/components/Component_FocusedEntityRef.scss +3 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/modules/ModuleFE_FocusedObject.d.ts +57 -0
- package/modules/ModuleFE_FocusedObject.js +161 -0
- package/modules/module-pack.d.ts +3 -0
- package/modules/module-pack.js +5 -0
- package/package.json +66 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ComponentSync } from '@nu-art/thunderstorm-frontend/index';
|
|
2
|
+
import { FocusData_Map, FocusedEntity } from '@nu-art/ts-focused-object-shared';
|
|
3
|
+
import { UniqueId } from '@nu-art/ts-common';
|
|
4
|
+
import { OnFocusedDataReceived } from '../modules/ModuleFE_FocusedObject.js';
|
|
5
|
+
import './Component_FocusedEntityRef.scss';
|
|
6
|
+
type Props = {
|
|
7
|
+
focusedEntities?: FocusedEntity[];
|
|
8
|
+
ignoreCurrentUser?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type State = {
|
|
11
|
+
focusedEntities?: FocusedEntity[];
|
|
12
|
+
ignoreCurrentUser?: boolean;
|
|
13
|
+
accountIds: UniqueId[];
|
|
14
|
+
};
|
|
15
|
+
export declare class Component_FocusedEntityRef extends ComponentSync<Props, State> implements OnFocusedDataReceived {
|
|
16
|
+
__onFocusedDataReceived(map: FocusData_Map): void;
|
|
17
|
+
protected deriveStateFromProps(nextProps: Props, state: State): State;
|
|
18
|
+
/**
|
|
19
|
+
* Mount / Unmount logic handled in
|
|
20
|
+
* - ComponentWillUnmount
|
|
21
|
+
* - ComponentDidMount
|
|
22
|
+
* - ComponentDidUpdate
|
|
23
|
+
*
|
|
24
|
+
* It must be the case, in order for un-focusing and focusing to happen in the correct order
|
|
25
|
+
* no matter how the component is rendered or recycled.
|
|
26
|
+
*/
|
|
27
|
+
componentWillUnmount(): void;
|
|
28
|
+
componentDidMount(): void;
|
|
29
|
+
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any): void;
|
|
30
|
+
render(): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ComponentSync, LL_H_C, Show } from '@nu-art/thunderstorm-frontend/index';
|
|
3
|
+
import { compare, filterDuplicates } from '@nu-art/ts-common';
|
|
4
|
+
import { ModuleFE_FocusedObject } from '../modules/ModuleFE_FocusedObject.js';
|
|
5
|
+
import { Component_AccountThumbnail, ModuleFE_Account } from '@nu-art/user-account-frontend/index';
|
|
6
|
+
import './Component_FocusedEntityRef.scss';
|
|
7
|
+
export class Component_FocusedEntityRef extends ComponentSync {
|
|
8
|
+
// ######################## Lifecycle ########################
|
|
9
|
+
__onFocusedDataReceived(map) {
|
|
10
|
+
this.reDeriveState();
|
|
11
|
+
}
|
|
12
|
+
deriveStateFromProps(nextProps, state) {
|
|
13
|
+
state.focusedEntities = nextProps.focusedEntities;
|
|
14
|
+
state.ignoreCurrentUser = nextProps.ignoreCurrentUser;
|
|
15
|
+
state.accountIds = state.focusedEntities?.reduce((accountIds, focusedEntity) => {
|
|
16
|
+
const accountIdsForFocusedItem = ModuleFE_FocusedObject.getAccountIdsForFocusedItem(focusedEntity.dbKey, focusedEntity.itemId, state.ignoreCurrentUser);
|
|
17
|
+
return filterDuplicates([...accountIds, ...accountIdsForFocusedItem]);
|
|
18
|
+
}, []) || [];
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Mount / Unmount logic handled in
|
|
23
|
+
* - ComponentWillUnmount
|
|
24
|
+
* - ComponentDidMount
|
|
25
|
+
* - ComponentDidUpdate
|
|
26
|
+
*
|
|
27
|
+
* It must be the case, in order for un-focusing and focusing to happen in the correct order
|
|
28
|
+
* no matter how the component is rendered or recycled.
|
|
29
|
+
*/
|
|
30
|
+
componentWillUnmount() {
|
|
31
|
+
if (this.state.focusedEntities)
|
|
32
|
+
ModuleFE_FocusedObject.unfocus(this.state.focusedEntities);
|
|
33
|
+
}
|
|
34
|
+
componentDidMount() {
|
|
35
|
+
//If mounted with focus entities, focus on them
|
|
36
|
+
if (this.state.focusedEntities)
|
|
37
|
+
ModuleFE_FocusedObject.focus(this.state.focusedEntities);
|
|
38
|
+
}
|
|
39
|
+
componentDidUpdate(prevProps, prevState, snapshot) {
|
|
40
|
+
//Change in focused entities, set new focused
|
|
41
|
+
if (!compare(prevState.focusedEntities, this.state.focusedEntities)) {
|
|
42
|
+
//Unfocus previous entities
|
|
43
|
+
if (this.state.focusedEntities)
|
|
44
|
+
ModuleFE_FocusedObject.unfocus(this.state.focusedEntities);
|
|
45
|
+
//focus current entities
|
|
46
|
+
if (prevState.focusedEntities)
|
|
47
|
+
ModuleFE_FocusedObject.focus(prevState.focusedEntities);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ######################## Render ########################
|
|
51
|
+
render() {
|
|
52
|
+
return _jsx(LL_H_C, { className: 'component--focused-object', children: this.state.accountIds.map(id => {
|
|
53
|
+
const account = ModuleFE_Account.cache.unique(id);
|
|
54
|
+
return _jsxs(Show, { children: [_jsx(Show.If, { condition: !!account, children: _jsx(Component_AccountThumbnail, { accountId: () => id }) }), _jsx(Show.Else, { children: _jsx("div", { children: "Bug" }) })] }, id);
|
|
55
|
+
}) });
|
|
56
|
+
}
|
|
57
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './modules/ModuleFE_FocusedObject.js';
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './modules/ModuleFE_FocusedObject.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Module, UniqueId } from '@nu-art/ts-common';
|
|
2
|
+
import { ApiDefCaller } from '@nu-art/thunderstorm-shared';
|
|
3
|
+
import { ThunderDispatcher } from '@nu-art/thunderstorm-frontend/index';
|
|
4
|
+
import { ApiStruct_FocusedObject, FocusData_Map, FocusedEntity } from '@nu-art/ts-focused-object-shared';
|
|
5
|
+
import { OnLoginStatusUpdated } from '@nu-art/user-account-frontend/index';
|
|
6
|
+
export interface OnFocusedDataReceived {
|
|
7
|
+
__onFocusedDataReceived: (map: FocusData_Map) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const dispatch_onFocusedDataReceived: ThunderDispatcher<OnFocusedDataReceived, "__onFocusedDataReceived", [map: FocusData_Map], void>;
|
|
10
|
+
export declare class ModuleFE_FocusedObject_Class extends Module implements OnLoginStatusUpdated {
|
|
11
|
+
readonly _v1: ApiDefCaller<ApiStruct_FocusedObject>['_v1'];
|
|
12
|
+
private focusFirebaseListener;
|
|
13
|
+
private focusDataMap;
|
|
14
|
+
private currentlyFocused;
|
|
15
|
+
private readonly apiDebounce;
|
|
16
|
+
private windowIsFocused;
|
|
17
|
+
private unfocusTimeout;
|
|
18
|
+
private keepAliveTimeout;
|
|
19
|
+
__onLoginStatusUpdated(): void;
|
|
20
|
+
constructor();
|
|
21
|
+
init(): void;
|
|
22
|
+
private initFirebaseListening;
|
|
23
|
+
private initWindowFocusListeners;
|
|
24
|
+
private initWindowCloseListeners;
|
|
25
|
+
private onRTDBChange;
|
|
26
|
+
/**
|
|
27
|
+
* Callback for when the current window is focused.
|
|
28
|
+
* we change the class property "windowIsFocused" to true so when the time comes to
|
|
29
|
+
* send a keepalive to BE, the request will be sent if the window is focused.
|
|
30
|
+
* Will also trigger keepalive timer if the time is not already set
|
|
31
|
+
*/
|
|
32
|
+
private onWindowFocus;
|
|
33
|
+
/**
|
|
34
|
+
* Callback for when the current window is un-focused.
|
|
35
|
+
* we change the class property "windowIsFocused" to false so when the time comes to
|
|
36
|
+
* send a keepalive to BE, the request will not be sent if the window is not focused
|
|
37
|
+
*/
|
|
38
|
+
private onWindowBlur;
|
|
39
|
+
private onUserLoggedOut;
|
|
40
|
+
private triggerKeepAlive;
|
|
41
|
+
private triggerUnfocus;
|
|
42
|
+
private clearKeepAlive;
|
|
43
|
+
private clearUnfocus;
|
|
44
|
+
private updateRTDB;
|
|
45
|
+
focus: (entities: FocusedEntity[]) => void;
|
|
46
|
+
unfocus: (entities: FocusedEntity[]) => void;
|
|
47
|
+
getFocusData: (dbKey: string, itemId: UniqueId) => {
|
|
48
|
+
[s: string]: {
|
|
49
|
+
[s: string]: {
|
|
50
|
+
[s: string]: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
getAccountIdsForFocusedItem: (dbKey: string, itemId: UniqueId, ignoreCurrentUser?: boolean) => UniqueId[];
|
|
55
|
+
private translateCurrentlyFocusedToFocusedEntities;
|
|
56
|
+
}
|
|
57
|
+
export declare const ModuleFE_FocusedObject: ModuleFE_FocusedObject_Class;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { _keys, debounce, filterDuplicates, Module, removeItemFromArray, Second } from '@nu-art/ts-common';
|
|
2
|
+
import { apiWithBody, ThunderDispatcher } from '@nu-art/thunderstorm-frontend/index';
|
|
3
|
+
import { ModuleFE_FirebaseListener } from '@nu-art/firebase-frontend/ModuleFE_FirebaseListener/ModuleFE_FirebaseListener';
|
|
4
|
+
import { ApiDef_FocusedObject, } from '@nu-art/ts-focused-object-shared';
|
|
5
|
+
import { LoggedStatus, ModuleFE_Account } from '@nu-art/user-account-frontend/index';
|
|
6
|
+
import { DefaultTTL_FocusedObject, getRelationalPath } from '@nu-art/ts-focused-object-shared/consts';
|
|
7
|
+
export const dispatch_onFocusedDataReceived = new ThunderDispatcher('__onFocusedDataReceived');
|
|
8
|
+
export class ModuleFE_FocusedObject_Class extends Module {
|
|
9
|
+
_v1;
|
|
10
|
+
focusFirebaseListener;
|
|
11
|
+
focusDataMap;
|
|
12
|
+
currentlyFocused = {};
|
|
13
|
+
apiDebounce;
|
|
14
|
+
windowIsFocused = true;
|
|
15
|
+
unfocusTimeout;
|
|
16
|
+
keepAliveTimeout;
|
|
17
|
+
__onLoginStatusUpdated() {
|
|
18
|
+
const status = ModuleFE_Account.getLoggedStatus();
|
|
19
|
+
if (status === LoggedStatus.LOGGED_OUT)
|
|
20
|
+
this.onUserLoggedOut();
|
|
21
|
+
}
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.focusDataMap = {};
|
|
25
|
+
this._v1 = {
|
|
26
|
+
// updateFocusData: apiWithBody(ApiDef_FocusedObject._v1.updateFocusData),
|
|
27
|
+
// setFocusStatusByTabId: apiWithBody(ApiDef_FocusedObject._v1.setFocusStatusByTabId),
|
|
28
|
+
// releaseObject: apiWithBody(ApiDef_FocusedObject._v1.releaseObject),
|
|
29
|
+
// releaseByTabId: apiWithBody(ApiDef_FocusedObject._v1.releaseByTabId),
|
|
30
|
+
update: apiWithBody(ApiDef_FocusedObject._v1.update),
|
|
31
|
+
};
|
|
32
|
+
this.apiDebounce = debounce(this.updateRTDB, 2 * Second, 10 * Second);
|
|
33
|
+
}
|
|
34
|
+
init() {
|
|
35
|
+
this.initFirebaseListening();
|
|
36
|
+
this.initWindowFocusListeners();
|
|
37
|
+
this.initWindowCloseListeners();
|
|
38
|
+
}
|
|
39
|
+
// ######################## Init listeners ########################
|
|
40
|
+
initFirebaseListening = () => {
|
|
41
|
+
this.focusFirebaseListener = ModuleFE_FirebaseListener.createListener(getRelationalPath());
|
|
42
|
+
this.focusFirebaseListener.startListening(this.onRTDBChange);
|
|
43
|
+
};
|
|
44
|
+
initWindowFocusListeners() {
|
|
45
|
+
window.addEventListener('focus', this.onWindowFocus);
|
|
46
|
+
window.addEventListener('blur', this.onWindowBlur);
|
|
47
|
+
}
|
|
48
|
+
initWindowCloseListeners() {
|
|
49
|
+
window.addEventListener('beforeunload', async (event) => {
|
|
50
|
+
this._v1.update({ focusedEntities: [] }).execute();
|
|
51
|
+
// navigator.sendBeacon('/log', JSON.stringify({ type:'application/json' }));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// ######################## Listener Callbacks ########################
|
|
55
|
+
onRTDBChange = (snapshot) => {
|
|
56
|
+
this.focusDataMap = snapshot.val() ?? {};
|
|
57
|
+
this.logDebug('Received firebase focus data', this.focusDataMap);
|
|
58
|
+
// Update all the FocusedEntityRef components
|
|
59
|
+
dispatch_onFocusedDataReceived.dispatchAll(this.focusDataMap);
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Callback for when the current window is focused.
|
|
63
|
+
* we change the class property "windowIsFocused" to true so when the time comes to
|
|
64
|
+
* send a keepalive to BE, the request will be sent if the window is focused.
|
|
65
|
+
* Will also trigger keepalive timer if the time is not already set
|
|
66
|
+
*/
|
|
67
|
+
onWindowFocus = () => {
|
|
68
|
+
this.windowIsFocused = true;
|
|
69
|
+
//If the keep alive counter still exists, no need to trigger any extra further logic
|
|
70
|
+
if (this.keepAliveTimeout)
|
|
71
|
+
return;
|
|
72
|
+
this.apiDebounce();
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Callback for when the current window is un-focused.
|
|
76
|
+
* we change the class property "windowIsFocused" to false so when the time comes to
|
|
77
|
+
* send a keepalive to BE, the request will not be sent if the window is not focused
|
|
78
|
+
*/
|
|
79
|
+
onWindowBlur = () => {
|
|
80
|
+
this.windowIsFocused = false;
|
|
81
|
+
};
|
|
82
|
+
onUserLoggedOut = () => {
|
|
83
|
+
this.currentlyFocused = {};
|
|
84
|
+
};
|
|
85
|
+
// ######################## Timer Interactions ########################
|
|
86
|
+
triggerKeepAlive = () => {
|
|
87
|
+
this.clearKeepAlive();
|
|
88
|
+
//No need to set keepalive timeout if currentlyFocused has no data
|
|
89
|
+
if (!_keys(this.currentlyFocused).length)
|
|
90
|
+
return;
|
|
91
|
+
this.keepAliveTimeout = setTimeout(() => {
|
|
92
|
+
//No need to keepalive if window is not focused
|
|
93
|
+
if (!this.windowIsFocused)
|
|
94
|
+
return this.clearKeepAlive();
|
|
95
|
+
this.apiDebounce();
|
|
96
|
+
}, DefaultTTL_FocusedObject - 20 * Second);
|
|
97
|
+
};
|
|
98
|
+
triggerUnfocus = () => {
|
|
99
|
+
this.clearUnfocus();
|
|
100
|
+
this.unfocusTimeout = setTimeout(() => this.apiDebounce(), 20 * Second);
|
|
101
|
+
};
|
|
102
|
+
clearKeepAlive = () => {
|
|
103
|
+
clearTimeout(this.keepAliveTimeout);
|
|
104
|
+
delete this.keepAliveTimeout;
|
|
105
|
+
};
|
|
106
|
+
clearUnfocus = () => {
|
|
107
|
+
clearTimeout(this.unfocusTimeout);
|
|
108
|
+
delete this.unfocusTimeout;
|
|
109
|
+
};
|
|
110
|
+
// ######################## API Logic ########################
|
|
111
|
+
updateRTDB = () => {
|
|
112
|
+
//Call API
|
|
113
|
+
const focusedEntities = this.translateCurrentlyFocusedToFocusedEntities();
|
|
114
|
+
this._v1.update({ focusedEntities })
|
|
115
|
+
.executeSync()
|
|
116
|
+
.then()
|
|
117
|
+
.catch(e => {
|
|
118
|
+
this.logError('Update focused object failed', e);
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
this.clearUnfocus();
|
|
122
|
+
this.triggerKeepAlive();
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
// ######################## Logic ########################
|
|
126
|
+
focus = (entities) => {
|
|
127
|
+
entities.forEach(entity => {
|
|
128
|
+
if (!this.currentlyFocused[entity.dbKey])
|
|
129
|
+
this.currentlyFocused[entity.dbKey] = [];
|
|
130
|
+
this.currentlyFocused[entity.dbKey] = filterDuplicates([...this.currentlyFocused[entity.dbKey], entity.itemId]);
|
|
131
|
+
});
|
|
132
|
+
this.apiDebounce();
|
|
133
|
+
};
|
|
134
|
+
unfocus = (entities) => {
|
|
135
|
+
entities.forEach(entity => {
|
|
136
|
+
if (!this.currentlyFocused[entity.dbKey])
|
|
137
|
+
return;
|
|
138
|
+
this.currentlyFocused[entity.dbKey] = removeItemFromArray(this.currentlyFocused[entity.dbKey], entity.itemId);
|
|
139
|
+
});
|
|
140
|
+
this.triggerUnfocus();
|
|
141
|
+
};
|
|
142
|
+
getFocusData = (dbKey, itemId) => {
|
|
143
|
+
return this.focusDataMap[dbKey]?.[itemId];
|
|
144
|
+
};
|
|
145
|
+
getAccountIdsForFocusedItem = (dbKey, itemId, ignoreCurrentUser = true) => {
|
|
146
|
+
const data = this.getFocusData(dbKey, itemId);
|
|
147
|
+
const userIds = data ? _keys(data) : [];
|
|
148
|
+
const account = ModuleFE_Account.getCurrentlyLoggedAccount();
|
|
149
|
+
return ignoreCurrentUser ? userIds.filter(id => id !== account?._id) : userIds;
|
|
150
|
+
};
|
|
151
|
+
translateCurrentlyFocusedToFocusedEntities = () => {
|
|
152
|
+
const focusedEntities = [];
|
|
153
|
+
_keys(this.currentlyFocused).forEach(dbKey => {
|
|
154
|
+
this.currentlyFocused[dbKey].forEach(itemId => {
|
|
155
|
+
focusedEntities.push({ dbKey: dbKey, itemId });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return focusedEntities;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export const ModuleFE_FocusedObject = new ModuleFE_FocusedObject_Class();
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nu-art/ts-focused-object-frontend",
|
|
3
|
+
"version": "0.400.5",
|
|
4
|
+
"description": "ts-focused-object - Express & Typescript based backend framework Frontend",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"TacB0sS",
|
|
7
|
+
"infra",
|
|
8
|
+
"nu-art",
|
|
9
|
+
"thunderstorm",
|
|
10
|
+
"typescript",
|
|
11
|
+
"ts-focused-object"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/nu-art-js/thunderstorm",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/nu-art-js/thunderstorm/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"directory": "dist",
|
|
19
|
+
"linkDirectory": true
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+ssh://git@github.com:nu-art-js/thunderstorm.git"
|
|
24
|
+
},
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"author": "TacB0sS",
|
|
27
|
+
"files": [
|
|
28
|
+
"**/*"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nu-art/ts-focused-object-shared": "0.400.5",
|
|
35
|
+
"@nu-art/firebase-frontend": "0.400.5",
|
|
36
|
+
"@nu-art/firebase-shared": "0.400.5",
|
|
37
|
+
"@nu-art/thunderstorm-frontend": "0.400.5",
|
|
38
|
+
"@nu-art/thunderstorm-shared": "0.400.5",
|
|
39
|
+
"@nu-art/ts-common": "0.400.5",
|
|
40
|
+
"@nu-art/user-account-frontend": "0.400.5",
|
|
41
|
+
"@nu-art/user-account-shared": "0.400.5",
|
|
42
|
+
"firebase": "^11.9.0",
|
|
43
|
+
"firebase-admin": "13.4.0",
|
|
44
|
+
"firebase-functions": "6.3.2",
|
|
45
|
+
"react": "^18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^18.0.0",
|
|
49
|
+
"@types/chai": "^4.3.4",
|
|
50
|
+
"@types/mocha": "^10.0.1"
|
|
51
|
+
},
|
|
52
|
+
"unitConfig": {
|
|
53
|
+
"type": "typescript-lib"
|
|
54
|
+
},
|
|
55
|
+
"type": "module",
|
|
56
|
+
"exports": {
|
|
57
|
+
".": {
|
|
58
|
+
"types": "./index.d.ts",
|
|
59
|
+
"import": "./index.js"
|
|
60
|
+
},
|
|
61
|
+
"./*": {
|
|
62
|
+
"types": "./*.d.ts",
|
|
63
|
+
"import": "./*.js"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|