2020-01-06 15:52:08 +01:00
|
|
|
|
/*
|
|
|
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-01-06 14:28:29 +01:00
|
|
|
|
import React from "react";
|
2021-10-13 12:09:43 +02:00
|
|
|
|
import { mount } from "enzyme";
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
|
|
|
|
import sdk from "../../../skinned-sdk";
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { mkEvent, mkStubRoom } from "../../../test-utils";
|
|
|
|
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
2020-01-06 15:38:21 +01:00
|
|
|
|
import * as languageHandler from "../../../../src/languageHandler";
|
2021-07-12 09:34:26 +02:00
|
|
|
|
import * as TestUtils from "../../../test-utils";
|
2021-07-16 21:30:26 +02:00
|
|
|
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
2021-07-12 09:34:26 +02:00
|
|
|
|
const _TextualBody = sdk.getComponent("views.messages.TextualBody");
|
|
|
|
|
const TextualBody = TestUtils.wrapInMatrixClientContext(_TextualBody);
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
|
|
|
|
describe("<TextualBody />", () => {
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
MatrixClientPeg.matrixClient = null;
|
|
|
|
|
});
|
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("renders m.emote correctly", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
MatrixClientPeg.matrixClient = {
|
|
|
|
|
getRoom: () => mkStubRoom("room_id"),
|
|
|
|
|
getAccountData: () => undefined,
|
2020-10-29 14:22:09 +01:00
|
|
|
|
isGuest: () => false,
|
2021-03-11 17:42:55 +01:00
|
|
|
|
mxcUrlToHttp: (s) => s,
|
2020-01-06 14:28:29 +01:00
|
|
|
|
};
|
2021-07-16 21:30:26 +02:00
|
|
|
|
DMRoomMap.makeShared();
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "winks",
|
|
|
|
|
msgtype: "m.emote",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("* sender winks");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe('<span class="mx_EventTile_body" dir="auto">winks</span>');
|
|
|
|
|
});
|
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("renders m.notice correctly", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
MatrixClientPeg.matrixClient = {
|
|
|
|
|
getRoom: () => mkStubRoom("room_id"),
|
|
|
|
|
getAccountData: () => undefined,
|
2020-10-29 14:22:09 +01:00
|
|
|
|
isGuest: () => false,
|
2021-03-11 17:42:55 +01:00
|
|
|
|
mxcUrlToHttp: (s) => s,
|
2020-01-06 14:28:29 +01:00
|
|
|
|
};
|
2021-07-16 21:30:26 +02:00
|
|
|
|
DMRoomMap.makeShared();
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "bot_sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "this is a notice, probably from a bot",
|
|
|
|
|
msgtype: "m.notice",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe(ev.getContent().body);
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("renders plain-text m.text correctly", () => {
|
2020-01-10 01:24:13 +01:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
MatrixClientPeg.matrixClient = {
|
|
|
|
|
getRoom: () => mkStubRoom("room_id"),
|
|
|
|
|
getAccountData: () => undefined,
|
2020-10-29 14:22:09 +01:00
|
|
|
|
isGuest: () => false,
|
2021-03-11 17:42:55 +01:00
|
|
|
|
mxcUrlToHttp: (s) => s,
|
2020-01-10 01:24:13 +01:00
|
|
|
|
};
|
2021-07-16 21:30:26 +02:00
|
|
|
|
DMRoomMap.makeShared();
|
2020-01-10 01:24:13 +01:00
|
|
|
|
});
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("simple message renders as expected", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "this is a plaintext message",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe(ev.getContent().body);
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe(`<span class="mx_EventTile_body" dir="auto">${ ev.getContent().body }</span>`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("linkification get applied correctly into the DOM", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "Visit https://matrix.org/",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe(ev.getContent().body);
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe('<span class="mx_EventTile_body" dir="auto">' +
|
2020-02-23 23:14:29 +01:00
|
|
|
|
'Visit <a href="https://matrix.org/" class="linkified" target="_blank" rel="noreferrer noopener">' +
|
2020-01-06 14:28:29 +01:00
|
|
|
|
'https://matrix.org/</a></span>');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("renders formatted m.text correctly", () => {
|
2020-01-10 01:24:13 +01:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
MatrixClientPeg.matrixClient = {
|
|
|
|
|
getRoom: () => mkStubRoom("room_id"),
|
|
|
|
|
getAccountData: () => undefined,
|
|
|
|
|
getUserId: () => "@me:my_server",
|
|
|
|
|
getHomeserverUrl: () => "https://my_server/",
|
|
|
|
|
on: () => undefined,
|
|
|
|
|
removeListener: () => undefined,
|
2020-10-29 14:22:09 +01:00
|
|
|
|
isGuest: () => false,
|
2021-03-11 17:42:55 +01:00
|
|
|
|
mxcUrlToHttp: (s) => s,
|
2020-01-10 01:24:13 +01:00
|
|
|
|
};
|
2021-07-16 21:30:26 +02:00
|
|
|
|
DMRoomMap.makeShared();
|
2020-01-10 01:24:13 +01:00
|
|
|
|
});
|
2020-01-06 14:28:29 +01:00
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("italics, bold, underline and strikethrough render as expected", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "foo *baz* __bar__ <del>del</del> <u>u</u>",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body: "foo <em>baz</em> <strong>bar</strong> <del>del</del> <u>u</u>",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("foo baz bar del u");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
|
|
|
|
ev.getContent().formatted_body + '</span>');
|
|
|
|
|
});
|
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("spoilers get injected properly into the DOM", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "Hey [Spoiler for movie](mxc://someserver/somefile)",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body: "Hey <span data-mx-spoiler=\"movie\">the movie was awesome</span>",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("Hey (movie) the movie was awesome");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
|
|
|
|
'Hey <span>' +
|
|
|
|
|
'<span class="mx_EventTile_spoiler">' +
|
|
|
|
|
'<span class="mx_EventTile_spoiler_reason">(movie)</span> ' +
|
|
|
|
|
'<span class="mx_EventTile_spoiler_content"><span>the movie was awesome</span></span>' +
|
|
|
|
|
'</span></span></span>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("pills get injected correctly into the DOM", () => {
|
2020-01-06 14:28:29 +01:00
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "Hey User",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body: "Hey <a href=\"https://matrix.to/#/@user:server\">Member</a>",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("Hey Member");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
|
|
|
|
'Hey <span>' +
|
2021-02-12 11:34:09 +01:00
|
|
|
|
'<a class="mx_Pill mx_UserPill">' +
|
2020-07-23 10:01:44 +02:00
|
|
|
|
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
|
|
|
|
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
2020-01-06 14:28:29 +01:00
|
|
|
|
'</span></span>');
|
|
|
|
|
});
|
2021-01-27 12:46:20 +01:00
|
|
|
|
|
|
|
|
|
it("pills do not appear for event permalinks", () => {
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body:
|
|
|
|
|
"An [event link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" +
|
|
|
|
|
"$16085560162aNpaH:example.com?via=example.com) with text",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body:
|
|
|
|
|
"An <a href=\"https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" +
|
|
|
|
|
"$16085560162aNpaH:example.com?via=example.com\">event link</a> with text",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("An event link with text");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe(
|
|
|
|
|
'<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
2022-01-20 18:18:47 +01:00
|
|
|
|
'An <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/' +
|
2021-01-27 12:46:20 +01:00
|
|
|
|
'$16085560162aNpaH:example.com?via=example.com" ' +
|
|
|
|
|
'rel="noreferrer noopener">event link</a> with text</span>',
|
|
|
|
|
);
|
|
|
|
|
});
|
2021-02-03 16:18:19 +01:00
|
|
|
|
|
|
|
|
|
it("pills appear for room links with vias", () => {
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body:
|
|
|
|
|
"A [room link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" +
|
|
|
|
|
"?via=example.com&via=bob.com) with vias",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body:
|
|
|
|
|
"A <a href=\"https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" +
|
|
|
|
|
"?via=example.com&via=bob.com\">room link</a> with vias",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
expect(wrapper.text()).toBe("A !ZxbRYPQXDXKGmDnJNg:example.com with vias");
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe(
|
|
|
|
|
'<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
2022-01-20 18:18:47 +01:00
|
|
|
|
'A <span><a class="mx_Pill mx_RoomPill" ' +
|
|
|
|
|
'href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com' +
|
2021-02-12 11:34:09 +01:00
|
|
|
|
'?via=example.com&via=bob.com"' +
|
|
|
|
|
'><img class="mx_BaseAvatar mx_BaseAvatar_image" ' +
|
2021-02-03 16:18:19 +01:00
|
|
|
|
'src="mxc://avatar.url/room.png" ' +
|
|
|
|
|
'style="width: 16px; height: 16px;" alt="" aria-hidden="true">' +
|
|
|
|
|
'!ZxbRYPQXDXKGmDnJNg:example.com</a></span> with vias</span>',
|
|
|
|
|
);
|
|
|
|
|
});
|
2021-12-02 10:25:12 +01:00
|
|
|
|
|
|
|
|
|
it('renders formatted body without html corretly', () => {
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "escaped \\*markdown\\*",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
format: "org.matrix.custom.html",
|
|
|
|
|
formatted_body: "escaped *markdown*",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} />);
|
|
|
|
|
|
|
|
|
|
const content = wrapper.find(".mx_EventTile_body");
|
|
|
|
|
expect(content.html()).toBe(
|
|
|
|
|
'<span class="mx_EventTile_body" dir="auto">' +
|
|
|
|
|
'escaped *markdown*' +
|
|
|
|
|
'</span>',
|
|
|
|
|
);
|
|
|
|
|
});
|
2020-01-06 14:28:29 +01:00
|
|
|
|
});
|
2020-01-06 15:38:21 +01:00
|
|
|
|
|
2020-01-10 01:24:13 +01:00
|
|
|
|
it("renders url previews correctly", () => {
|
2020-01-06 15:38:21 +01:00
|
|
|
|
languageHandler.setMissingEntryGenerator(key => key.split('|', 2)[1]);
|
|
|
|
|
|
|
|
|
|
MatrixClientPeg.matrixClient = {
|
|
|
|
|
getRoom: () => mkStubRoom("room_id"),
|
|
|
|
|
getAccountData: () => undefined,
|
|
|
|
|
getUrlPreview: (url) => new Promise(() => {}),
|
2021-01-14 18:39:58 +01:00
|
|
|
|
isGuest: () => false,
|
2021-03-11 17:42:55 +01:00
|
|
|
|
mxcUrlToHttp: (s) => s,
|
2020-01-06 15:38:21 +01:00
|
|
|
|
};
|
2021-07-16 21:30:26 +02:00
|
|
|
|
DMRoomMap.makeShared();
|
2020-01-06 15:38:21 +01:00
|
|
|
|
|
|
|
|
|
const ev = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
body: "Visit https://matrix.org/",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
|
|
|
|
|
2021-07-07 19:12:31 +02:00
|
|
|
|
const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />);
|
2020-01-06 15:38:21 +01:00
|
|
|
|
expect(wrapper.text()).toBe(ev.getContent().body);
|
|
|
|
|
|
2021-07-12 09:34:26 +02:00
|
|
|
|
let widgets = wrapper.find("LinkPreviewGroup");
|
|
|
|
|
// at this point we should have exactly one link
|
|
|
|
|
expect(widgets.at(0).prop("links")).toEqual(["https://matrix.org/"]);
|
2020-01-06 15:38:21 +01:00
|
|
|
|
|
|
|
|
|
// simulate an event edit and check the transition from the old URL preview to the new one
|
|
|
|
|
const ev2 = mkEvent({
|
|
|
|
|
type: "m.room.message",
|
|
|
|
|
room: "room_id",
|
|
|
|
|
user: "sender",
|
|
|
|
|
content: {
|
|
|
|
|
"m.new_content": {
|
|
|
|
|
body: "Visit https://vector.im/ and https://riot.im/",
|
|
|
|
|
msgtype: "m.text",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
event: true,
|
|
|
|
|
});
|
2022-02-21 17:57:44 +01:00
|
|
|
|
jest.spyOn(ev, 'replacingEventDate').mockReturnValue(new Date(1993, 7, 3));
|
2020-01-06 15:38:21 +01:00
|
|
|
|
ev.makeReplaced(ev2);
|
|
|
|
|
|
|
|
|
|
wrapper.setProps({
|
|
|
|
|
mxEvent: ev,
|
|
|
|
|
replacingEventId: ev.getId(),
|
|
|
|
|
}, () => {
|
|
|
|
|
expect(wrapper.text()).toBe(ev2.getContent()["m.new_content"].body + "(edited)");
|
|
|
|
|
|
|
|
|
|
// XXX: this is to give TextualBody enough time for state to settle
|
|
|
|
|
wrapper.setState({}, () => {
|
2021-07-12 09:34:26 +02:00
|
|
|
|
widgets = wrapper.find("LinkPreviewGroup");
|
|
|
|
|
// at this point we should have exactly two links (not the matrix.org one anymore)
|
|
|
|
|
expect(widgets.at(0).prop("links")).toEqual(["https://vector.im/", "https://riot.im/"]);
|
2020-01-06 15:38:21 +01:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2020-01-06 14:28:29 +01:00
|
|
|
|
});
|
|
|
|
|
|