Mercurial > xmpp-account-manager
annotate avatar.js @ 63:ee1df80a1715 default tip
Nicer-looking input form
author | mathieui |
---|---|
date | Sun, 24 May 2020 14:19:29 +0200 |
parents | 6d861d881b96 |
children |
rev | line source |
---|---|
60
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
1 // SPDX-License-Identifier: AGPL-3.0-only |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
2 /* |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
3 * Copyright © 2018-2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
4 * Copyright © 2020 Mathieu Pasquet <mathieui@mathieui.net> |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
5 * |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
6 * Released under GNU AGPL v3 only, read the file 'LICENSE' for more information. |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
7 */ |
6d861d881b96
Add license headers to all source files.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
52
diff
changeset
|
8 |
0 | 9 'use strict'; |
10 | |
11 function initAvatar(connection) { | |
43
aff7caa10489
Encode the default avatar using encodeURIComponent().
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
22
diff
changeset
|
12 const DEFAULT_AVATAR = 'data:image/svg+xml,' + encodeURIComponent('<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 150 150"><rect width="150" height="150" fill="#888" stroke-width="1" stroke="#000"/><text x="75" y="100" text-anchor="middle" font-size="100">?</text></svg>'); |
0 | 13 |
14 const avatar_data = {}; | |
15 const avatar_img = document.getElementById('avatar'); | |
16 const avatar_size = document.getElementById('avatar-size'); | |
17 const avatar_file = document.getElementById('avatar-file'); | |
18 const avatar_upload = document.getElementById('avatar-upload'); | |
19 const avatar_change = document.getElementById('avatar-change'); | |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
20 const avatar_access = document.getElementById('avatar-access'); |
11
aedf80eefc19
Also use a spinner on avatar get/set.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
21 const spinner_img = document.getElementById('avatar-spinner'); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
22 const access_spinner_img = document.getElementById('access-model-avatar-spinner'); |
0 | 23 |
24 avatar_img.src = DEFAULT_AVATAR; | |
25 const iq = $iq({type: 'get'}) | |
26 .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) | |
27 .c('items', {node: 'urn:xmpp:avatar:metadata'}); | |
28 connection.sendIQ(iq, onAvatarMetadata, onAvatarRetrievalError.bind(null, 'PubSub metadata query failed.')); | |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
29 displaySpinner(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
30 getAccessModel(); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
31 |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
32 function getAccessModel() |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
33 { |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
34 avatar_access.disabled = true; |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
35 displaySpinner(access_spinner_img); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
36 retrieveConfiguration(connection, 'urn:xmpp:avatar:metadata').then((access_model) => { |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
37 if (access_model !== null) { |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
38 if (access_model === 'open') |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
39 avatar_access.value = 'open'; |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
40 else if (access_model === 'presence') |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
41 avatar_access.value = 'presence'; |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
42 else |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
43 console.log('Unsupported avatar access model: ' + access_model); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
44 avatar_access.disabled = false; |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
45 } |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
46 hideSpinner(access_spinner_img); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
47 }, (reason) => { |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
48 console.log('Failed to retrieve avatar configuration: ' + reason); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
49 hideSpinner(access_spinner_img); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
50 }); |
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
51 } |
0 | 52 |
53 function onAvatarMetadata(result_iq) | |
54 { | |
55 const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item'); | |
56 if (item == null) | |
57 return onAvatarRetrievalError('no item found.'); | |
58 const id = item.getAttributeNS(null, 'id'); | |
59 const info = parseXPath(item, './avatar_metadata:metadata/avatar_metadata:info'); | |
60 if (info == null) | |
61 return onAvatarRetrievalError('no info found, your avatar metadata node is broken.'); | |
62 if (id != info.getAttributeNS(null, 'id')) | |
63 return onAvatarRetrievalError('invalid id in metadata.'); | |
64 | |
65 const parsed_info = { | |
66 id: id, | |
67 type: info.getAttributeNS(null, 'type'), | |
68 bytes: info.getAttributeNS(null, 'bytes'), | |
69 width: info.getAttributeNS(null, 'width'), | |
70 height: info.getAttributeNS(null, 'height'), | |
71 }; | |
72 const iq = $iq({type: 'get'}) | |
73 .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) | |
74 .c('items', {node: 'urn:xmpp:avatar:data'}) | |
75 .c('item', {id: id}); | |
76 connection.sendIQ(iq, onAvatarData.bind(null, parsed_info), onAvatarRetrievalError.bind(null, 'PubSub data query failed.')); | |
77 } | |
78 | |
79 function onAvatarData(info, result_iq) | |
80 { | |
81 const item = parseXPath(result_iq, './pubsub:pubsub/pubsub:items/pubsub:item'); | |
82 if (item == null) | |
83 return onAvatarRetrievalError('no item found.'); | |
84 if (info.id != item.getAttributeNS(null, 'id')) | |
85 return onAvatarRetrievalError('invalid id in data.'); | |
86 | |
87 const data = parseXPath(item, './avatar_data:data').textContent; | |
88 const url = 'data:' + info.type + ';base64,' + data; | |
89 // TODO: validate the bytes too. | |
90 /* | |
91 // TODO: figure out why this didn’t work. | |
92 avatar_img.onload = function (evt) { | |
93 const img = evt.target; | |
94 if (img.naturalWidth != info.width || img.naturalHeight != info.height) | |
95 return onAvatarRetrievalError('invalid width or height in image data.'); | |
96 avatar_img.onload = null; | |
97 }; | |
98 */ | |
99 avatar_img.src = url; | |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
100 hideSpinner(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
101 getAccessModel(); |
0 | 102 } |
103 | |
104 function onAvatarRetrievalError(string) | |
105 { | |
106 console.log('Failed to retrieve avatar, an empty one is displayed instead: ' + string); | |
107 avatar_img.src = DEFAULT_AVATAR; | |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
108 hideSpinner(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
109 avatar_access.disabled = true; |
0 | 110 } |
111 | |
112 avatar_upload.addEventListener('click', function (evt) { | |
113 avatar_file.click(); | |
114 }); | |
115 | |
116 avatar_change.addEventListener('click', function (evt) { | |
117 const metadata_iq = $iq({type: 'set'}) | |
118 .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) | |
119 .c('publish', {node: 'urn:xmpp:avatar:metadata'}) | |
120 .c('item', {id: avatar_data.id}) | |
121 .c('metadata', {xmlns: 'urn:xmpp:avatar:metadata'}) | |
122 .c('info', { | |
123 id: avatar_data.id, | |
124 type: avatar_data.type, | |
125 bytes: avatar_data.bytes, | |
126 width: avatar_img.naturalWidth, | |
127 height: avatar_img.naturalHeight, | |
128 }); | |
129 connection.sendIQ(metadata_iq, onAvatarMetadataUpload, onAvatarUploadError); | |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
130 displaySpinner(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
131 avatar_access.disabled = true; |
11
aedf80eefc19
Also use a spinner on avatar get/set.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
132 }); |
aedf80eefc19
Also use a spinner on avatar get/set.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
133 |
aedf80eefc19
Also use a spinner on avatar get/set.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
134 function onAvatarMetadataUpload(iq) |
aedf80eefc19
Also use a spinner on avatar get/set.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
3
diff
changeset
|
135 { |
0 | 136 const data_iq = $iq({type: 'set'}) |
137 .c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'}) | |
138 .c('publish', {node: 'urn:xmpp:avatar:data'}) | |
139 .c('item', {id: avatar_data.id}) | |
140 .c('data', {xmlns: 'urn:xmpp:avatar:data'}) | |
141 .t(avatar_data.data); | |
142 connection.sendIQ(data_iq, onAvatarDataUpload, onAvatarUploadError); | |
143 } | |
144 | |
145 function onAvatarDataUpload(iq) | |
146 { | |
147 console.log('Avatar successfully uploaded!', iq); | |
13
8724e28ccbd7
Improve styling.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
11
diff
changeset
|
148 avatar_change.hidden = true; |
0 | 149 avatar_size.innerHTML = ''; |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
150 spinnerOk(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
151 getAccessModel(); |
0 | 152 } |
153 | |
154 function onAvatarUploadError(iq) | |
155 { | |
156 console.log("onAvatarUploadError", iq); | |
15
3eed9fe0bd7c
End spinners with either a green ✔ or a red ✘.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
13
diff
changeset
|
157 spinnerError(spinner_img); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
158 avatar_access.disabled = true; |
0 | 159 } |
160 | |
161 avatar_file.addEventListener('change', function (evt) { | |
162 const file = evt.target.files[0]; | |
163 avatar_data.type = file.type; | |
164 avatar_data.bytes = file.size; | |
165 | |
166 // Set the preview. | |
167 avatar_img.src = URL.createObjectURL(file); | |
168 const [size, unit] = friendlyDataSize(file.size); | |
169 avatar_size.innerHTML = Math.round(size) + ' ' + unit; | |
170 | |
171 // Obtain the base64 version of this file. | |
172 const base64_reader = new FileReader(); | |
173 base64_reader.onload = function (evt) { | |
174 const data = evt.target.result; | |
175 avatar_data.data = data.substr(data.indexOf(',') + 1); | |
176 } | |
177 base64_reader.readAsDataURL(file); | |
178 | |
179 // Compute the sha1 of this file. | |
180 const sha1_reader = new FileReader(); | |
181 sha1_reader.onload = async function (evt) { | |
182 const data = evt.target.result; | |
183 const digest = await window.crypto.subtle.digest('SHA-1', data); | |
184 const sha1 = (Array | |
185 .from(new Uint8Array(digest)) | |
186 .map(b => b.toString(16).padStart(2, "0")) | |
187 .join("")); | |
188 avatar_data.id = sha1; | |
13
8724e28ccbd7
Improve styling.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
11
diff
changeset
|
189 avatar_change.hidden = false; |
0 | 190 } |
191 sha1_reader.readAsArrayBuffer(file); | |
192 }); | |
193 | |
194 function friendlyDataSize(bytes) { | |
195 let unit = 'B' | |
196 if (bytes >= 1024) { | |
197 bytes /= 1024; | |
198 unit = 'KiB'; | |
199 } | |
200 if (bytes >= 1024) { | |
201 bytes /= 1024; | |
202 unit = 'MiB'; | |
203 } | |
204 if (bytes >= 1024) { | |
205 bytes /= 1024; | |
206 unit = 'GiB'; | |
207 } | |
208 if (bytes >= 1024) { | |
209 bytes /= 1024; | |
210 unit = 'TiB'; | |
211 } | |
212 return [bytes, unit]; | |
213 } | |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
214 |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
215 avatar_access.addEventListener('change', function (evt) { |
3
5aa1bf7154b0
Add a simple PEP node viewer and editor.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
2
diff
changeset
|
216 const iq = configurePEPField('urn:xmpp:avatar:metadata', 'pubsub#access_model', evt.target.value); |
21
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
217 connection.sendIQ(iq, onAvatarMetadataConfigured, onAvatarConfigureError.bind(null, 'PubSub configuration of metadata failed.')); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
218 displaySpinner(access_spinner_img); |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
219 }); |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
220 |
21
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
221 function onAvatarMetadataConfigured(result_iq) |
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
222 { |
22
07e33207e598
Fixup for an unitialised variable in the previous commit.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
21
diff
changeset
|
223 const iq = configurePEPField('urn:xmpp:avatar:data', 'pubsub#access_model', avatar_access.value); |
21
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
224 connection.sendIQ(iq, onAvatarConfigured, onAvatarConfigureError.bind(null, 'PubSub configuration of data failed.')); |
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
225 } |
cd35420457ac
Also configure the avatar data node’s access model.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
15
diff
changeset
|
226 |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
227 function onAvatarConfigured(result_iq) |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
228 { |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
229 console.log('Successfully set avatar access model.') |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
230 spinnerOk(access_spinner_img); |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
231 } |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
232 |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
233 function onAvatarConfigureError(string) |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
234 { |
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
235 console.log('Failed to configure avatar node: ' + string); |
52
2f45bee88b47
Add pubsub#access_model retrieval for the avatar node.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
43
diff
changeset
|
236 spinnerError(access_spinner_img); |
2
db033e5eabcb
Add pubsub#access_model configuration for avatars.
Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
parents:
1
diff
changeset
|
237 } |
0 | 238 } |