Compare commits
694 Commits
work/calls
...
v24.01.80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259b9884c7 | ||
|
|
2980dc49e4 | ||
|
|
5e12f50899 | ||
|
|
79940f707f | ||
|
|
e7024a270c | ||
|
|
1a561f6649 | ||
|
|
2cbb6ed65c | ||
|
|
ab4639926a | ||
|
|
89b6c54f25 | ||
|
|
27c9c62564 | ||
|
|
bb8ffb02d1 | ||
|
|
201aa82c04 | ||
|
|
704430db7a | ||
|
|
dcbbbd9296 | ||
|
|
410258c478 | ||
|
|
6baf2e4888 | ||
|
|
dd6eaac556 | ||
|
|
840903128c | ||
|
|
68f0ca96da | ||
|
|
faf7af06fe | ||
|
|
4ef67c3e0d | ||
|
|
5efd17d370 | ||
|
|
0dbef58ff2 | ||
|
|
6a3df8baf4 | ||
|
|
2fc973f218 | ||
|
|
7d16999c44 | ||
|
|
f9ba31f2dc | ||
|
|
d4ad773ff1 | ||
|
|
822a4dc500 | ||
|
|
bfd1d431c1 | ||
|
|
94bf2481f0 | ||
|
|
5c32520c35 | ||
|
|
de47597f6e | ||
|
|
7780a72888 | ||
|
|
1f8c07fedf | ||
|
|
877575b4d3 | ||
|
|
1181df4db2 | ||
|
|
5be2113b32 | ||
|
|
7ad7af56e8 | ||
|
|
d3148f8c8b | ||
|
|
100f595026 | ||
|
|
15ba6d58e2 | ||
|
|
61ad892732 | ||
|
|
2a3e1dfcd7 | ||
|
|
d1dc6fc4ed | ||
|
|
6dc30a9ca7 | ||
|
|
ae0c5ffaef | ||
|
|
fc546d4a43 | ||
|
|
96e62e3ebe | ||
|
|
d979cd2fbc | ||
|
|
22298181cb | ||
|
|
1312fde470 | ||
|
|
7fe2feb1e4 | ||
|
|
67fb5d0824 | ||
|
|
359114bd3d | ||
|
|
3db9f1198b | ||
|
|
2435a6b953 | ||
|
|
e6c8b3fa4b | ||
|
|
2d55dca508 | ||
|
|
bbb0bc3092 | ||
|
|
4065aa6a2e | ||
|
|
5942eac5ed | ||
|
|
85c2b7dada | ||
|
|
86ef921cdb | ||
|
|
aab69c5bae | ||
|
|
624578ec77 | ||
|
|
197ff984fd | ||
|
|
a26337d5f4 | ||
|
|
31b4eefadd | ||
|
|
b8e592f8ba | ||
|
|
555d23863e | ||
|
|
ffa2d5dc0e | ||
|
|
9987edbaf2 | ||
|
|
d90298392d | ||
|
|
600dbd0603 | ||
|
|
2179e2cc35 | ||
|
|
8119ea3ccb | ||
|
|
3fc125a798 | ||
|
|
99f6778df4 | ||
|
|
f7bd24db34 | ||
|
|
41c2f9c4d5 | ||
|
|
56d5f3036b | ||
|
|
21bb7dce21 | ||
|
|
11081719a7 | ||
|
|
980de7d85d | ||
|
|
7735313b0c | ||
|
|
1a3055df86 | ||
|
|
84cad630cd | ||
|
|
0beb5df08d | ||
|
|
4ef44b8e93 | ||
|
|
59153be006 | ||
|
|
50be96f762 | ||
|
|
819d88e18c | ||
|
|
dbbad2cf13 | ||
|
|
08b84c6592 | ||
|
|
b8abf0540d | ||
|
|
7f9e709559 | ||
|
|
c4f6abee9d | ||
|
|
1a4947b98a | ||
|
|
6ba2b715c3 | ||
|
|
e9e1e223f7 | ||
|
|
006da1fb16 | ||
|
|
3aff1795c8 | ||
|
|
576b1f928f | ||
|
|
fc3ab50701 | ||
|
|
e7fa3ad524 | ||
|
|
5adffddbd8 | ||
|
|
1d95d5aa15 | ||
|
|
dabd6291a5 | ||
|
|
0e55c3b38f | ||
|
|
ff4cf86ea5 | ||
|
|
f4d5ccbf12 | ||
|
|
192601d358 | ||
|
|
d7c432119e | ||
|
|
eddb2b73c2 | ||
|
|
0c60bfdb83 | ||
|
|
2bfb2fa1f9 | ||
|
|
d1aac971bf | ||
|
|
284cadf305 | ||
|
|
4697b7fcf1 | ||
|
|
9f356912c9 | ||
|
|
1cf891f845 | ||
|
|
52e2d636b9 | ||
|
|
dc6b539ddf | ||
|
|
e3cf85aa8c | ||
|
|
2065eb6684 | ||
|
|
9cac2a8abd | ||
|
|
feb87e6f70 | ||
|
|
a0057b8a49 | ||
|
|
dbb0269354 | ||
|
|
7fdb617b33 | ||
|
|
d798b0dec9 | ||
|
|
95cf23eb5b | ||
|
|
7e3db20229 | ||
|
|
12689babfb | ||
|
|
211407da44 | ||
|
|
400a84e48d | ||
|
|
7a45640e5e | ||
|
|
8af20885ab | ||
|
|
4033f07272 | ||
|
|
5df4fa297d | ||
|
|
33c5b418d2 | ||
|
|
69d378a17b | ||
|
|
f1b1b8ce53 | ||
|
|
526d4748e0 | ||
|
|
c5f93adbf4 | ||
|
|
61630cbe90 | ||
|
|
8c435e9d6d | ||
|
|
57978b1a6e | ||
|
|
f3c4d9449a | ||
|
|
9b37777f20 | ||
|
|
d6d6c161db | ||
|
|
772bca5ba6 | ||
|
|
036a60a095 | ||
|
|
b3315e1ed4 | ||
|
|
d300e9cf52 | ||
|
|
1f4bcd150f | ||
|
|
9ad8894983 | ||
|
|
9e63ca5eb7 | ||
|
|
ade66242bb | ||
|
|
2c6932b4cb | ||
|
|
5cce9e7205 | ||
|
|
7dd3ad9548 | ||
|
|
f690b76efa | ||
|
|
87b8d6710e | ||
|
|
965b890346 | ||
|
|
02b4e5cc70 | ||
|
|
410add04fb | ||
|
|
b575b1e700 | ||
|
|
5828ee1ada | ||
|
|
a64c80109e | ||
|
|
de47f7f2fa | ||
|
|
19adc7b9e5 | ||
|
|
23f60a59fe | ||
|
|
946ba2e56d | ||
|
|
9dcb7b49fa | ||
|
|
52f5901642 | ||
|
|
d4b4a7e1ff | ||
|
|
4449678b74 | ||
|
|
19e197e0ec | ||
|
|
cfc5202645 | ||
|
|
9b80d9e7aa | ||
|
|
dc409387bd | ||
|
|
1e73a7bda4 | ||
|
|
ef1d62d45c | ||
|
|
c2d82750b1 | ||
|
|
c97d276b36 | ||
|
|
681a0b1e93 | ||
|
|
39556f45ab | ||
|
|
3c7774800a | ||
|
|
bc7530eaa1 | ||
|
|
82d11f79d6 | ||
|
|
25d0368d41 | ||
|
|
83b7e7d121 | ||
|
|
26fd26f9fd | ||
|
|
0029567c3a | ||
|
|
c7614caf41 | ||
|
|
6571dbe554 | ||
|
|
b3a29068cc | ||
|
|
5adda55a85 | ||
|
|
d56f0d6086 | ||
|
|
60772be391 | ||
|
|
27f1679741 | ||
|
|
838596c3ae | ||
|
|
a57744891a | ||
|
|
f5417a6227 | ||
|
|
ac6f9ea219 | ||
|
|
4b49559d39 | ||
|
|
baa33f1843 | ||
|
|
dae5718c6c | ||
|
|
60260cff3b | ||
|
|
2df9a26cdc | ||
|
|
b16cd12b33 | ||
|
|
6a3b22ef2d | ||
|
|
b28a85ff05 | ||
|
|
e480299563 | ||
|
|
fe70e2773f | ||
|
|
e78ea4721a | ||
|
|
1699dcf0c4 | ||
|
|
9d6aef6c2b | ||
|
|
a9c2428498 | ||
|
|
0730f15e2b | ||
|
|
136856f3c3 | ||
|
|
763b6af076 | ||
|
|
ac231320a3 | ||
|
|
87aee162f1 | ||
|
|
0899db31af | ||
|
|
b4198bc13b | ||
|
|
c6bfe73d26 | ||
|
|
d490dffa36 | ||
|
|
43b2b71b73 | ||
|
|
39a51d1f35 | ||
|
|
2eb26ffbb3 | ||
|
|
87ef55215f | ||
|
|
f6186aad2e | ||
|
|
2251edbf86 | ||
|
|
1c55649740 | ||
|
|
aa0b6613de | ||
|
|
f948e813b6 | ||
|
|
b5c6411aad | ||
|
|
b1daa76d9f | ||
|
|
7180fa022b | ||
|
|
17bc08270d | ||
|
|
d4cb27eca4 | ||
|
|
541350e678 | ||
|
|
843deefaf8 | ||
|
|
070d579bc2 | ||
|
|
add283c9fb | ||
|
|
fe4230b5fd | ||
|
|
e8f40d98de | ||
|
|
eba62103a4 | ||
|
|
925393deab | ||
|
|
abe881caf7 | ||
|
|
237a3c9dfb | ||
|
|
9715440854 | ||
|
|
ecdad9f965 | ||
|
|
08711fc927 | ||
|
|
e44cd405b7 | ||
|
|
8945e004e2 | ||
|
|
c04d8d6f59 | ||
|
|
58a73c0208 | ||
|
|
852110debd | ||
|
|
6b71d3c78d | ||
|
|
f3a0adee39 | ||
|
|
6e7b6c9ce0 | ||
|
|
f67cd7deb5 | ||
|
|
931b4b1f9a | ||
|
|
167ed4eca3 | ||
|
|
7d5b2c1b6a | ||
|
|
be7b1e49b4 | ||
|
|
957419070a | ||
|
|
f22107c8ab | ||
|
|
3a4f71de7f | ||
|
|
4ed4f3f628 | ||
|
|
ba24f1272f | ||
|
|
443661d113 | ||
|
|
091c8806db | ||
|
|
041c719a2e | ||
|
|
83a9bfa974 | ||
|
|
e35a6f7257 | ||
|
|
6d56251f6f | ||
|
|
486fae9c10 | ||
|
|
1c26d9b811 | ||
|
|
6d7ae99c94 | ||
|
|
442a343097 | ||
|
|
f0a7216b4b | ||
|
|
e926b22524 | ||
|
|
69087c2117 | ||
|
|
4d2104b54b | ||
|
|
3f85a359e1 | ||
|
|
3084913940 | ||
|
|
e2670cd6ba | ||
|
|
1b6fc3dde5 | ||
|
|
69a19effa2 | ||
|
|
4abdf1f920 | ||
|
|
0b1a6a3f6b | ||
|
|
45544c79bb | ||
|
|
c4dddf6e02 | ||
|
|
7f3f628b7d | ||
|
|
6bf552398e | ||
|
|
78f676d71a | ||
|
|
33c0cae64c | ||
|
|
14cdd096cf | ||
|
|
c04ddfde26 | ||
|
|
ec4c156a8c | ||
|
|
17ff5b4c56 | ||
|
|
0e2275e415 | ||
|
|
12fd1875b5 | ||
|
|
10e50804c7 | ||
|
|
c01c638a49 | ||
|
|
399151eb1d | ||
|
|
5e80715898 | ||
|
|
6439fa48f9 | ||
|
|
823f3cdd4e | ||
|
|
f542d0b9fd | ||
|
|
a43990559b | ||
|
|
feb2dbc9fb | ||
|
|
f299d5a245 | ||
|
|
d69b8fbf8c | ||
|
|
234e5c49c4 | ||
|
|
cee72b6d48 | ||
|
|
539fdcaf2e | ||
|
|
32b3861c3e | ||
|
|
f1076a5ced | ||
|
|
bf8f5705d0 | ||
|
|
2656a93ee7 | ||
|
|
cbab810a2e | ||
|
|
e78dfaec34 | ||
|
|
3cfa773820 | ||
|
|
94c0e8b6cd | ||
|
|
35b1f24cb7 | ||
|
|
de3072125e | ||
|
|
f7d2ffac66 | ||
|
|
ff0990bb7c | ||
|
|
8285961c42 | ||
|
|
cd39d5b129 | ||
|
|
83b3fefbf5 | ||
|
|
3718bd716a | ||
|
|
a18257ee17 | ||
|
|
def46d90a8 | ||
|
|
8afd7b2892 | ||
|
|
deb11367cb | ||
|
|
ffd1b06a82 | ||
|
|
2597f0aec9 | ||
|
|
33ca72efd9 | ||
|
|
2fbf659eb4 | ||
|
|
763198b2c3 | ||
|
|
50551541ed | ||
|
|
323cd4962e | ||
|
|
b2f592afeb | ||
|
|
f7f98c43e2 | ||
|
|
06134f96eb | ||
|
|
bb62849a19 | ||
|
|
d05385caed | ||
|
|
209ca747b5 | ||
|
|
2c6ab498ac | ||
|
|
597633f824 | ||
|
|
65da416b8e | ||
|
|
c528929b30 | ||
|
|
8650ce1755 | ||
|
|
8936b23da9 | ||
|
|
cf72fbea2e | ||
|
|
0d5929b4bc | ||
|
|
d43cc9a044 | ||
|
|
682e3967ba | ||
|
|
ceebee3a56 | ||
|
|
52ab052aef | ||
|
|
e6a2b2d125 | ||
|
|
92dadc2cc9 | ||
|
|
6ca9380a51 | ||
|
|
51d41ab5bf | ||
|
|
f1047fa474 | ||
|
|
011b11681f | ||
|
|
8baeb236bc | ||
|
|
b9d173d0b5 | ||
|
|
3790955c56 | ||
|
|
199772a013 | ||
|
|
54cc3ac761 | ||
|
|
1675c14c92 | ||
|
|
56f5ef2611 | ||
|
|
7ba63eb680 | ||
|
|
ddd690f6d5 | ||
|
|
d9dc5f48bc | ||
|
|
a3b40a5e6d | ||
|
|
be71a4349b | ||
|
|
ede2707767 | ||
|
|
5871903529 | ||
|
|
87745c360d | ||
|
|
40d2eb5aba | ||
|
|
cfd9f36a97 | ||
|
|
48f7e06c01 | ||
|
|
43ecd188f1 | ||
|
|
063056bebd | ||
|
|
dfba655527 | ||
|
|
23178b6224 | ||
|
|
a9a6e31b8c | ||
|
|
0afd74d96a | ||
|
|
c5501f2b5d | ||
|
|
68602fc177 | ||
|
|
8ce5a57499 | ||
|
|
bf3c5708c7 | ||
|
|
8359c1fe90 | ||
|
|
774121ea8c | ||
|
|
370bbf8a4b | ||
|
|
4fd36fb221 | ||
|
|
167349b9d2 | ||
|
|
296aa9ddad | ||
|
|
94c1b67ec9 | ||
|
|
a9965acdd3 | ||
|
|
35313271a3 | ||
|
|
e40179f641 | ||
|
|
dacf3ff8a0 | ||
|
|
0274d70f71 | ||
|
|
0c19052f32 | ||
|
|
a3423e5724 | ||
|
|
e09f31f891 | ||
|
|
e05e0f5489 | ||
|
|
002961f638 | ||
|
|
e28419ef88 | ||
|
|
0e4df58fc9 | ||
|
|
21a55885b3 | ||
|
|
6a8ac8f6e5 | ||
|
|
9fcfad7058 | ||
|
|
cfcc1756dd | ||
|
|
e15bec2295 | ||
|
|
9071cf827f | ||
|
|
c7deaaba84 | ||
|
|
414035de8b | ||
|
|
442612d31d | ||
|
|
59164d3bb2 | ||
|
|
cc60dde62d | ||
|
|
cbed8148a3 | ||
|
|
50e8b9ebf6 | ||
|
|
f5ad2ad162 | ||
|
|
d11d6c74b3 | ||
|
|
63ed69a5d4 | ||
|
|
a8aa775575 | ||
|
|
6305359b3c | ||
|
|
7e859364af | ||
|
|
8abd0db012 | ||
|
|
dbc10685f0 | ||
|
|
96582a12bc | ||
|
|
0ac61854bd | ||
|
|
68298f038d | ||
|
|
405fd5841a | ||
|
|
5da9bba844 | ||
|
|
84373712ef | ||
|
|
50f4f96341 | ||
|
|
1b27b1a4e2 | ||
|
|
33811a4c49 | ||
|
|
43715486e5 | ||
|
|
5c72bd4ab7 | ||
|
|
092f1be99b | ||
|
|
550d55cb1a | ||
|
|
e9edb61245 | ||
|
|
89aae665b1 | ||
|
|
1e3c3dd1f4 | ||
|
|
6f4d2c0216 | ||
|
|
803cd2b4e4 | ||
|
|
9f3012061d | ||
|
|
c1604a9c4f | ||
|
|
3c7fcee244 | ||
|
|
d33a50a00d | ||
|
|
ed033a1c5e | ||
|
|
2b961703ae | ||
|
|
0539665779 | ||
|
|
df127b88e6 | ||
|
|
27c4b57f0f | ||
|
|
f875a23e83 | ||
|
|
7f5cfbf21c | ||
|
|
f7c7643c1c | ||
|
|
692edf52f1 | ||
|
|
e48cfaa41f | ||
|
|
ba116460d5 | ||
|
|
c71672dab3 | ||
|
|
932ef72311 | ||
|
|
7e53a2234f | ||
|
|
a0499e5140 | ||
|
|
093ef0a18c | ||
|
|
36bf862ab9 | ||
|
|
6d8c1d0780 | ||
|
|
7fe85066a4 | ||
|
|
1fefa228e6 | ||
|
|
4104e10d95 | ||
|
|
05f3c3ee0a | ||
|
|
0a1c489401 | ||
|
|
e53d63ad8b | ||
|
|
78541b32f0 | ||
|
|
3bd93996c0 | ||
|
|
61968aa475 | ||
|
|
24d0082048 | ||
|
|
2980af11b0 | ||
|
|
91d9406c38 | ||
|
|
280d1e38e2 | ||
|
|
7b520da4b4 | ||
|
|
419b4cea98 | ||
|
|
38824f30ac | ||
|
|
9c4d8ef823 | ||
|
|
fe3bf3a638 | ||
|
|
d678a446e2 | ||
|
|
6df60a39b0 | ||
|
|
6f9fa76ab7 | ||
|
|
6e8b0f001f | ||
|
|
0bfad95d8b | ||
|
|
1b43846196 | ||
|
|
4ea183b139 | ||
|
|
22fd24549b | ||
|
|
af793fb865 | ||
|
|
13414b5834 | ||
|
|
65a1df4a75 | ||
|
|
d15157703a | ||
|
|
2d21330a36 | ||
|
|
fbc4611d02 | ||
|
|
e257865b19 | ||
|
|
018154f555 | ||
|
|
80a5daa1f7 | ||
|
|
bf5ce049d9 | ||
|
|
58213ee3e6 | ||
|
|
2d1862a637 | ||
|
|
67453d9fb8 | ||
|
|
3ab04583ae | ||
|
|
794683b5d2 | ||
|
|
9f7f5409bd | ||
|
|
7d7b9d7ae0 | ||
|
|
c7c892c51c | ||
|
|
97903a7cb0 | ||
|
|
0c8f488ac4 | ||
|
|
88368cefaa | ||
|
|
ab4bb41979 | ||
|
|
c2398b19dc | ||
|
|
4b3dac025f | ||
|
|
9eeff06328 | ||
|
|
657a8291d0 | ||
|
|
bc4431462a | ||
|
|
fdd8a62873 | ||
|
|
c164228894 | ||
|
|
7d418f0883 | ||
|
|
e90aecbead | ||
|
|
221c27fbbe | ||
|
|
1907404124 | ||
|
|
81e953dd8c | ||
|
|
9311052e39 | ||
|
|
648fff20f2 | ||
|
|
980211f36a | ||
|
|
25e0e3fa8e | ||
|
|
403c1ed391 | ||
|
|
c2f76612cf | ||
|
|
2a651b8946 | ||
|
|
00c0d1c276 | ||
|
|
0162d91b60 | ||
|
|
5f41b7b4c1 | ||
|
|
d9691bac7c | ||
|
|
719b916837 | ||
|
|
f9514fe925 | ||
|
|
399b9e6944 | ||
|
|
00cf5c8a5b | ||
|
|
02f26424e2 | ||
|
|
3bd639775a | ||
|
|
163c1ef407 | ||
|
|
9997b1074a | ||
|
|
f52a25717b | ||
|
|
5746c24b73 | ||
|
|
dca1b22521 | ||
|
|
b15f1461d8 | ||
|
|
24fd7a0f5b | ||
|
|
0ac3932303 | ||
|
|
807a9ee779 | ||
|
|
4f32a168cd | ||
|
|
45d2da56c7 | ||
|
|
d794e26d62 | ||
|
|
42cc42642d | ||
|
|
ffd6205bd1 | ||
|
|
9ba6b2f0a5 | ||
|
|
f8d3e25f8e | ||
|
|
75cd77facb | ||
|
|
8546d76a54 | ||
|
|
00b7f68a03 | ||
|
|
b486cb905c | ||
|
|
cdd5c9f484 | ||
|
|
3e6c426397 | ||
|
|
847f41e1ad | ||
|
|
ed5bb270ff | ||
|
|
c8b4da2b96 | ||
|
|
ba5445e135 | ||
|
|
1cca39e105 | ||
|
|
dbf67b984e | ||
|
|
c9126cf38e | ||
|
|
13988da4fc | ||
|
|
0847839abc | ||
|
|
6b55e502a0 | ||
|
|
8f81629ac1 | ||
|
|
7f459cb90f | ||
|
|
420e195313 | ||
|
|
3263a69880 | ||
|
|
b060881f06 | ||
|
|
701e786c1f | ||
|
|
646c8ba8fe | ||
|
|
9b31fdea10 | ||
|
|
ce5dfdee16 | ||
|
|
918e805718 | ||
|
|
7debf47833 | ||
|
|
2c142c36e6 | ||
|
|
3279142498 | ||
|
|
0e08a1aa7e | ||
|
|
3855922ad2 | ||
|
|
89a094ce8f | ||
|
|
d4421aaecf | ||
|
|
80b451c27b | ||
|
|
32ff22ba89 | ||
|
|
b990e3fbcb | ||
|
|
8d1baab89f | ||
|
|
62ecbc8d6b | ||
|
|
0e1c7f8c47 | ||
|
|
c963966f1d | ||
|
|
8db2526153 | ||
|
|
7587a1a418 | ||
|
|
294812956a | ||
|
|
d01aac6fa3 | ||
|
|
5cda1ca3ce | ||
|
|
bf26a463e0 | ||
|
|
30180c1ac8 | ||
|
|
41c296061c | ||
|
|
461420af0a | ||
|
|
052a7ed571 | ||
|
|
98e86f887b | ||
|
|
63a17b5985 | ||
|
|
025b367a7e | ||
|
|
a8536b0634 | ||
|
|
0c43d3eeee | ||
|
|
bf1614256b | ||
|
|
99614d1b27 | ||
|
|
e1e57ef27a | ||
|
|
6d45d126f8 | ||
|
|
732b43cbd6 | ||
|
|
a5da17b000 | ||
|
|
10a294f99e | ||
|
|
882a0d4901 | ||
|
|
506d31f53f | ||
|
|
110f007b41 | ||
|
|
a90943d9ac | ||
|
|
cbe7d8c2c2 | ||
|
|
ae4943dd71 | ||
|
|
7bd84bf51e | ||
|
|
a6ce44eb24 | ||
|
|
b1c42c3d3d | ||
|
|
213aaf3ac4 | ||
|
|
c55b40c9c6 | ||
|
|
81928d8b93 | ||
|
|
b7bddba053 | ||
|
|
307a9370db | ||
|
|
2f65cbeb36 | ||
|
|
e0c0b1f0e8 | ||
|
|
74f767aa82 | ||
|
|
7176dd4476 | ||
|
|
f75fe31571 | ||
|
|
61bdb1ed5f | ||
|
|
527e9d93a5 | ||
|
|
7d22b30217 | ||
|
|
c0d2333a3d | ||
|
|
4a7e1d058c | ||
|
|
66974615f6 | ||
|
|
116c888686 | ||
|
|
f6d6a804d2 | ||
|
|
6d6d702b97 | ||
|
|
9be04c1272 | ||
|
|
6f5d88cf63 | ||
|
|
1103e80191 | ||
|
|
a5320397c2 | ||
|
|
53719b971d | ||
|
|
64a1316f9b | ||
|
|
a3b8168744 | ||
|
|
c06e69931a | ||
|
|
30f8573dfc | ||
|
|
7f067b698e | ||
|
|
b56ebdf149 | ||
|
|
a02dd4ab87 | ||
|
|
898f0c962a | ||
|
|
5b4ae764cf | ||
|
|
d14db326bb | ||
|
|
5c51e0d0fc | ||
|
|
f78b4af692 | ||
|
|
bd5ed0f46c | ||
|
|
529cfa8f7d | ||
|
|
5552cd60f6 | ||
|
|
c51a1f4851 | ||
|
|
a27f4765e4 | ||
|
|
402f99923c | ||
|
|
2afda78912 | ||
|
|
25f9c7e125 | ||
|
|
05082cb2bb | ||
|
|
59495a1452 | ||
|
|
d10460c45b | ||
|
|
b968c85de2 | ||
|
|
13efc08b07 |
10
.craft.ini
Normal file
10
.craft.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
; SPDX-FileCopyrightText: None
|
||||
; SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[BlueprintSettings]
|
||||
kde/unreleased/kirigami-addons.version=master
|
||||
kde/frameworks.version=master
|
||||
kde/libs.version=master
|
||||
kde/plasma.version=master
|
||||
kde/unreleased.version=master
|
||||
libs/qt.qtMajorVersion=6
|
||||
@@ -19,6 +19,7 @@
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.kde.kwalletd5",
|
||||
"--talk-name=org.kde.StatusNotifierWatcher",
|
||||
"--talk-name=org.freedesktop.secrets",
|
||||
"--own-name=org.kde.StatusNotifierItem-2-2"
|
||||
],
|
||||
"modules": [
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
build
|
||||
build*
|
||||
.clang-format
|
||||
.DS_Store
|
||||
.kdev4/
|
||||
@@ -11,3 +11,4 @@ kate.project.ctags.*
|
||||
.flatpak-builder/
|
||||
.idea/
|
||||
cmake-build-*
|
||||
src/res.generated.qrc
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
include:
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/reuse-lint.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/android-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/linux-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/windows-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd.yml
|
||||
# - https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/freebsd-qt6.yml
|
||||
- https://invent.kde.org/sysadmin/ci-utilities/raw/master/gitlab-templates/flatpak.yml
|
||||
- project: sysadmin/ci-utilities
|
||||
file:
|
||||
- /gitlab-templates/reuse-lint.yml
|
||||
- /gitlab-templates/android-qt6.yml
|
||||
- /gitlab-templates/linux-qt6.yml
|
||||
- /gitlab-templates/windows-qt6.yml
|
||||
- /gitlab-templates/freebsd-qt6.yml
|
||||
# - /gitlab-templates/flatpak.yml
|
||||
- /gitlab-templates/craft-android-qt6-apks.yml
|
||||
|
||||
45
.kde-ci.yml
45
.kde-ci.yml
@@ -2,35 +2,7 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
Dependencies:
|
||||
- 'on': ['Linux/Qt5', 'Android/Qt5', 'FreeBSD/Qt5', 'Windows/Qt5']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@stable'
|
||||
'frameworks/kcoreaddons': '@stable'
|
||||
'frameworks/kirigami': '@stable'
|
||||
'frameworks/ki18n': '@stable'
|
||||
'frameworks/kconfig': '@stable'
|
||||
'frameworks/syntax-highlighting': '@stable'
|
||||
'frameworks/kitemmodels': '@stable'
|
||||
'frameworks/kquickcharts': '@stable'
|
||||
'frameworks/knotifications': '@stable'
|
||||
'libraries/kquickimageeditor': '@stable'
|
||||
'frameworks/sonnet': '@stable'
|
||||
'libraries/kirigami-addons': '@latest'
|
||||
'third-party/libquotient': '@latest'
|
||||
'third-party/qtkeychain': '@latest'
|
||||
'third-party/cmark': '@latest'
|
||||
'third-party/qcoro': '@latest'
|
||||
- 'on': ['Windows/Qt5', 'Linux/Qt5', 'FreeBSD/Qt5']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@stable'
|
||||
'frameworks/kio': '@stable'
|
||||
'frameworks/kwindowsystem': '@stable'
|
||||
'frameworks/kconfigwidgets': '@stable'
|
||||
- 'on': ['Linux/Qt5', 'FreeBSD/Qt5']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@stable'
|
||||
|
||||
- 'on': ['Linux/Qt6', 'Android/Qt6', 'FreeBSD/Qt6', 'Windows/Qt6']
|
||||
- 'on': ['Linux', 'Android', 'FreeBSD', 'Windows']
|
||||
'require':
|
||||
'frameworks/extra-cmake-modules': '@latest-kf6'
|
||||
'frameworks/kcoreaddons': '@latest-kf6'
|
||||
@@ -41,22 +13,29 @@ Dependencies:
|
||||
'frameworks/kitemmodels': '@latest-kf6'
|
||||
'frameworks/kquickcharts': '@latest-kf6'
|
||||
'frameworks/knotifications': '@latest-kf6'
|
||||
'frameworks/kcolorscheme': '@latest-kf6'
|
||||
'libraries/kquickimageeditor': '@latest-kf6'
|
||||
'frameworks/sonnet': '@latest-kf6'
|
||||
'frameworks/prison': '@latest-kf6'
|
||||
'libraries/kirigami-addons': '@latest-kf6'
|
||||
'third-party/libquotient': '@latest'
|
||||
'third-party/qtkeychain': '@latest'
|
||||
'third-party/cmark': '@latest'
|
||||
'third-party/qcoro': '@latest'
|
||||
- 'on': ['Windows/Qt6', 'Linux/Qt6', 'FreeBSD/Qt6']
|
||||
- 'on': ['Windows', 'Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/qqc2-desktop-style': '@latest-kf6'
|
||||
'frameworks/kio': '@latest-kf6'
|
||||
'frameworks/kwindowsystem': '@latest-kf6'
|
||||
'frameworks/kconfigwidgets': '@latest-kf6'
|
||||
- 'on': ['Linux/Qt6', 'FreeBSD/Qt6']
|
||||
'frameworks/kstatusnotifieritem': '@latest-kf6'
|
||||
- 'on': ['Linux', 'FreeBSD']
|
||||
'require':
|
||||
'frameworks/kdbusaddons': '@latest-kf6'
|
||||
|
||||
- 'on': ['Linux']
|
||||
'require':
|
||||
'sdk/selenium-webdriver-at-spi': '@latest-kf6'
|
||||
|
||||
Options:
|
||||
require-passing-tests-on: [ 'Linux/Qt5', 'FreeBSD' ]
|
||||
per-test-timeout: 90
|
||||
require-passing-tests-on: [ '@all' ]
|
||||
|
||||
10
.reuse/dep5
10
.reuse/dep5
@@ -34,10 +34,18 @@ Files: src/neochat.notifyrc
|
||||
Copyright: 2020 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: src/qml/Component/confetti.png src/qml/Component/glowdot.png
|
||||
Files: src/qml/confetti.png src/qml/glowdot.png
|
||||
Copyright: 2021 Alexey Andreyev <aa13q@ya.ru>
|
||||
License: CC0-1.0
|
||||
|
||||
Files: .flatpak-manifest.json
|
||||
Copyright: 2020-2022 Tobias Fella <tobias.fella@kde.org>
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: autotests/data/*
|
||||
Copyright: none
|
||||
License: CC0-1.0
|
||||
|
||||
Files: appiumtests/data/*
|
||||
Copyright: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
License: CC0-1.0
|
||||
|
||||
104
CMakeLists.txt
104
CMakeLists.txt
@@ -7,18 +7,15 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# KDE Applications version, managed by release script.
|
||||
set(RELEASE_SERVICE_VERSION_MAJOR "23")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "07")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "70")
|
||||
set(RELEASE_SERVICE_VERSION_MAJOR "24")
|
||||
set(RELEASE_SERVICE_VERSION_MINOR "01")
|
||||
set(RELEASE_SERVICE_VERSION_MICRO "80")
|
||||
set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}")
|
||||
|
||||
project(NeoChat VERSION ${RELEASE_SERVICE_VERSION})
|
||||
|
||||
set(KF_MIN_VERSION "5.105.0")
|
||||
set(QT_MIN_VERSION "5.15.2")
|
||||
if (ANDROID)
|
||||
set(QT_MIN_VERSION "5.15.8")
|
||||
endif()
|
||||
set(KF_MIN_VERSION "5.240.0")
|
||||
set(QT_MIN_VERSION "6.5")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
@@ -27,7 +24,7 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.84)
|
||||
set(KDE_COMPILERSETTINGS_LEVEL 5.105)
|
||||
|
||||
include(FeatureSummary)
|
||||
include(ECMSetupVersion)
|
||||
@@ -40,6 +37,7 @@ include(ECMAddAppIcon)
|
||||
include(KDEGitCommitHooks)
|
||||
include(ECMCheckOutboundLicense)
|
||||
include(ECMQtDeclareLoggingCategory)
|
||||
include(ECMAddAndroidApk)
|
||||
if (NOT ANDROID)
|
||||
include(KDEClangFormat)
|
||||
endif()
|
||||
@@ -48,32 +46,28 @@ if(NEOCHAT_FLATPAK)
|
||||
include(cmake/Flatpak.cmake)
|
||||
endif()
|
||||
|
||||
set(QUOTIENT_FORCE_NAMESPACED_INCLUDES TRUE)
|
||||
|
||||
ecm_setup_version(${PROJECT_VERSION}
|
||||
VARIABLE_PREFIX NEOCHAT
|
||||
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h
|
||||
)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView)
|
||||
set_package_properties(Qt6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} COMPONENTS Kirigami2 I18n Notifications Config CoreAddons Sonnet ItemModels)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION} PROPERTIES
|
||||
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Kirigami I18n Notifications Config CoreAddons Sonnet ItemModels ColorScheme)
|
||||
set_package_properties(KF6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Basic application components"
|
||||
)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
find_package(KF${QT_MAJOR_VERSION}KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION}Keychain)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
|
||||
set_package_properties(KF6Kirigami PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Secure storage of account secrets"
|
||||
PURPOSE "Kirigami application UI framework"
|
||||
)
|
||||
find_package(KF6KirigamiAddons 0.7.2 REQUIRED)
|
||||
|
||||
if(ANDROID)
|
||||
find_package(OpenSSL)
|
||||
@@ -82,30 +76,37 @@ if(ANDROID)
|
||||
PURPOSE "Encrypted communications"
|
||||
)
|
||||
else()
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle ConfigWidgets KIO WindowSystem)
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}QQC2DesktopStyle PROPERTIES
|
||||
find_package(Qt6 ${QT_MIN_VERSION} COMPONENTS Widgets)
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS QQC2DesktopStyle KIO WindowSystem StatusNotifierItem)
|
||||
set_package_properties(KF6QQC2DesktopStyle PROPERTIES
|
||||
TYPE RUNTIME
|
||||
)
|
||||
ecm_find_qmlmodule(org.kde.syntaxhighlighting 1.0)
|
||||
|
||||
find_package(ICU 61.0 COMPONENTS uc)
|
||||
set_package_properties(ICU PROPERTIES
|
||||
TYPE REQUIRED
|
||||
PURPOSE "Unicode library"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
find_package(KF${QT_MAJOR_VERSION}DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
find_package(KF6DBusAddons ${KF_MIN_VERSION} REQUIRED)
|
||||
endif()
|
||||
|
||||
if(QT_MAJOR_VERSION STREQUAL "6" AND NOT ANDROID AND NOT WIN32)
|
||||
set(QUOTIENT_SUFFIX "Qt6")
|
||||
endif()
|
||||
|
||||
find_package(Quotient${QUOTIENT_SUFFIX} 0.6)
|
||||
set_package_properties(Quotient${QUOTIENT_SUFFIX} PROPERTIES
|
||||
find_package(QuotientQt6 0.7)
|
||||
set_package_properties(QuotientQt6 PROPERTIES
|
||||
TYPE REQUIRED
|
||||
DESCRIPTION "Qt wrapper around Matrix API"
|
||||
URL "https://github.com/quotient-im/libQuotient/"
|
||||
PURPOSE "Talk with matrix server"
|
||||
)
|
||||
|
||||
if (NOT TARGET Olm::Olm)
|
||||
message(FATAL_ERROR "NeoChat requires Quotient with the E2EE feature enabled")
|
||||
endif()
|
||||
|
||||
|
||||
find_package(cmark)
|
||||
set_package_properties(cmark PROPERTIES
|
||||
TYPE REQUIRED
|
||||
@@ -117,6 +118,8 @@ set_package_properties(cmark PROPERTIES
|
||||
ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0)
|
||||
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
|
||||
ecm_find_qmlmodule(org.kde.quickcharts 1.0)
|
||||
ecm_find_qmlmodule(QtLocation)
|
||||
ecm_find_qmlmodule(org.kde.prison)
|
||||
|
||||
find_package(KQuickImageEditor COMPONENTS)
|
||||
set_package_properties(KQuickImageEditor PROPERTIES
|
||||
@@ -126,33 +129,28 @@ set_package_properties(KQuickImageEditor PROPERTIES
|
||||
PURPOSE "Add image editing capability to image attachments"
|
||||
)
|
||||
|
||||
find_package(QCoro${QT_MAJOR_VERSION} 0.4 COMPONENTS Core REQUIRED)
|
||||
find_package(QCoro6 0.4 COMPONENTS Core Network REQUIRED)
|
||||
|
||||
qcoro_enable_coroutines()
|
||||
|
||||
find_package(KF${QT_MAJOR_VERSION}DocTools ${KF_MIN_VERSION})
|
||||
set_package_properties(KF${QT_MAJOR_VERSION}DocTools PROPERTIES DESCRIPTION
|
||||
find_package(KF6DocTools ${KF_MIN_VERSION})
|
||||
set_package_properties(KF6DocTools PROPERTIES DESCRIPTION
|
||||
"Tools to generate documentation"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
|
||||
if(NOT Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
|
||||
cmake_policy(SET CMP0063 OLD)
|
||||
endif()
|
||||
find_package(KUnifiedPush QUIET)
|
||||
set_package_properties(KUnifiedPush PROPERTIES
|
||||
TYPE OPTIONAL
|
||||
PURPOSE "Push notification support"
|
||||
URL "https://invent.kde.org/libraries/kunifiedpush"
|
||||
)
|
||||
|
||||
if(ANDROID)
|
||||
find_package(Sqlite3)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/android/version.gradle.in ${CMAKE_BINARY_DIR}/version.gradle)
|
||||
endif()
|
||||
|
||||
include(FindPkgConfig)
|
||||
pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>1.18 gstreamer-webrtc-1.0>=1.18)
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
add_feature_info(voip ON "GStreamer found. Call support is enabled.")
|
||||
else()
|
||||
add_feature_info(voip OFF "GStreamer not found. Call support is disabled.")
|
||||
endif()
|
||||
|
||||
ki18n_install(po)
|
||||
|
||||
install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
@@ -163,12 +161,14 @@ install(FILES org.kde.neochat.tray.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/h
|
||||
add_definitions(-DQT_NO_FOREACH)
|
||||
|
||||
add_subdirectory(src)
|
||||
if (BUILD_TESTING AND Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
|
||||
|
||||
if (BUILD_TESTING)
|
||||
find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test)
|
||||
add_subdirectory(autotests)
|
||||
add_subdirectory(appiumtests)
|
||||
endif()
|
||||
|
||||
if(KF${QT_MAJOR_VERSION}DocTools_FOUND)
|
||||
if(KF6DocTools_FOUND)
|
||||
kdoctools_install(po)
|
||||
add_subdirectory(doc)
|
||||
endif()
|
||||
@@ -186,3 +186,9 @@ file(GLOB_RECURSE ALL_SOURCE_FILES *.cpp *.h *.qml)
|
||||
# Fixes the test by excluding this directory
|
||||
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX [[_(install|build)/.*]])
|
||||
ecm_check_outbound_license(LICENSES GPL-3.0-only FILES ${ALL_SOURCE_FILES})
|
||||
|
||||
ecm_qt_install_logging_categories(
|
||||
EXPORT NEOCHAT
|
||||
FILE neochat.categories
|
||||
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
|
||||
)
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
|
||||
A Qt/QML based Matrix client.
|
||||
|
||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/projects/images/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||
<a href='https://matrix.org'><img src='https://matrix.org/docs/legacy/made-for-matrix.png' alt='Made for Matrix' height=64 target=_blank /></a>
|
||||
<a href='https://flathub.org/apps/details/org.kde.neochat'><img width='190px' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-i-en.png'/></a>
|
||||
<a href='https://snapcraft.io/neochat'><img width='190px' alt='Download on the Snap Store' src='https://snapcraft.io/static/images/badges/en/snap-store-black.svg'/></a>
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -95,7 +96,7 @@ As is the case throughout the KDE ecosystem contributions are welcome from all.
|
||||
|
||||
## Contact
|
||||
|
||||
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://matrix.to/#/#neochat:kde.org).
|
||||
The best place to reach the maintainers is on the KDE Matrix instance in the NeoChat channel, [#neochat:kde.org](https://go.kde.org/matrix/#/#neochat:kde.org). See [Matrix](https://community.kde.org/Matrix) for more details.
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
android:versionName="${versionName}"
|
||||
android:versionCode="${versionCode}"
|
||||
android:installLocation="auto">
|
||||
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat">
|
||||
<application android:name="org.qtproject.qt.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
|
||||
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
|
||||
android:name="org.qtproject.qt5.android.bindings.QtActivity"
|
||||
android:name="org.qtproject.qt.android.bindings.QtActivity"
|
||||
android:label="NeoChat"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTop"
|
||||
|
||||
@@ -12,7 +12,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ android {
|
||||
* The following variables:
|
||||
* - androidBuildToolsVersion,
|
||||
* - androidCompileSdkVersion
|
||||
* - qt5AndroidDir - holds the path to qt android files
|
||||
* - qtAndroidDir - holds the path to qt android files
|
||||
* needed to build any Qt application
|
||||
* on Android.
|
||||
*
|
||||
@@ -44,16 +44,20 @@ android {
|
||||
* Changing them manually might break the compilation!
|
||||
*******************************************************/
|
||||
|
||||
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||
|
||||
compileSdkVersion androidCompileSdkVersion
|
||||
buildToolsVersion androidBuildToolsVersion
|
||||
ndkVersion androidNdkVersion
|
||||
|
||||
// Extract native libraries from the APK
|
||||
packagingOptions.jniLibs.useLegacyPackaging true
|
||||
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
manifest.srcFile 'AndroidManifest.xml'
|
||||
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
|
||||
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
|
||||
res.srcDirs = [qt5AndroidDir + '/res', 'res']
|
||||
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
|
||||
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
|
||||
res.srcDirs = [qtAndroidDir + '/res', 'res']
|
||||
resources.srcDirs = ['src']
|
||||
renderscript.srcDirs = ['src']
|
||||
assets.srcDirs = ['assets']
|
||||
|
||||
28
appiumtests/CMakeLists.txt
Normal file
28
appiumtests/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
|
||||
|
||||
|
||||
if(NOT BUILD_TESTING OR NOT CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(SeleniumWebDriverATSPI)
|
||||
set_package_properties(SeleniumWebDriverATSPI PROPERTIES
|
||||
DESCRIPTION "Server component for selenium tests using Linux accessibility infrastructure"
|
||||
PURPOSE "Needed for GUI tests"
|
||||
URL "https://invent.kde.org/sdk/selenium-webdriver-at-spi"
|
||||
TYPE OPTIONAL
|
||||
)
|
||||
if(NOT SeleniumWebDriverATSPI_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_test(
|
||||
NAME logintest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/logintest.py
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME openuserdetailstest
|
||||
COMMAND selenium-webdriver-at-spi-run ${CMAKE_CURRENT_SOURCE_DIR}/openuserdetailstest.py
|
||||
)
|
||||
3
appiumtests/data/sync_response_no_rooms.json
Normal file
3
appiumtests/data/sync_response_no_rooms.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"next_batch": "batch1234"
|
||||
}
|
||||
50
appiumtests/data/sync_response_rooms.json
Normal file
50
appiumtests/data/sync_response_rooms.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"next_batch": "batch1234",
|
||||
"rooms": {
|
||||
"join": {
|
||||
"!room_id_1234:localhost:1234": {
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.member",
|
||||
"state_key": "@user:localhost:1234",
|
||||
"sender": "@user:localhost:1234",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"event_id": "$event_id_1234_0:localhost:1234",
|
||||
"room_id": "!room_id_1234:localhost:1234",
|
||||
"content": {
|
||||
"avatar_url": "",
|
||||
"displayname": "A Display Name",
|
||||
"membership": "join",
|
||||
"reason": "Nothing"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.message",
|
||||
"sender": "@user:localhost:1234",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"event_id": "$event_id_1234_1:localhost:1234",
|
||||
"room_id": "!room_id_1234:localhost:1234",
|
||||
"content": {
|
||||
"body": "This is a message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href=\"https://matrix.to/#/@user:localhost:1234\">User</a>:",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
appiumtests/login-server.py
Normal file
70
appiumtests/login-server.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
import json
|
||||
from flask import Flask, request, abort
|
||||
import os
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/_matrix/client/v3/login", methods=["GET"])
|
||||
def login_get():
|
||||
result = dict()
|
||||
result["flows"] = [dict()]
|
||||
result["flows"][0]["type"] = "m.login.password"
|
||||
return result
|
||||
|
||||
@app.route("/_matrix/client/v3/account/whoami", methods=["GET"])
|
||||
def whoami():
|
||||
result = dict()
|
||||
result["device_id"] = "device_id_1234"
|
||||
result["user_id"] = "@user:localhost:1234"
|
||||
return result
|
||||
|
||||
@app.route("/_matrix/client/v3/login", methods=["POST"])
|
||||
def login_post():
|
||||
data = request.get_json()
|
||||
if data["identifier"]["user"] != "user" or data["password"] != "1234":
|
||||
abort(403)
|
||||
print(data)
|
||||
result = dict()
|
||||
result["access_token"] = "token_login"
|
||||
result["device_id"] = "device_1234"
|
||||
result["user_id"] = "@user:localhost:1234"
|
||||
return result
|
||||
|
||||
def load_json(name):
|
||||
parts = __file__.split("/")
|
||||
parts.pop()
|
||||
datadir = "/".join(parts)
|
||||
return json.loads(open(f"{datadir}/data/{name}.json").read())
|
||||
|
||||
|
||||
@app.route("/_matrix/client/r0/sync")
|
||||
def sync():
|
||||
|
||||
result = load_json("sync_response_no_rooms") if ("login" in request.headers.get("Authorization")) else load_json("sync_response_rooms")
|
||||
return result
|
||||
|
||||
@app.route("/.well-known/matrix/client")
|
||||
def well_known():
|
||||
reply = dict()
|
||||
reply["m.homeserver"] = dict()
|
||||
reply["m.homeserver"]["base_url"] = "https://localhost:1234"
|
||||
return reply
|
||||
|
||||
@app.route("/_matrix/client/v3/profile/<id>")
|
||||
def profile(id):
|
||||
reply = dict()
|
||||
reply["avatar_url"] = "mxc://localhost:1234/asdf1234"
|
||||
reply["displayname"] = "User123"
|
||||
return reply
|
||||
|
||||
@app.route("/_matrix/client/v3/keys/upload", methods=["POST"])
|
||||
def upload_keys():
|
||||
reply = dict()
|
||||
return reply
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(ssl_context='adhoc', port=1234)
|
||||
51
appiumtests/logintest.py
Executable file
51
appiumtests/logintest.py
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from appium import webdriver
|
||||
from appium.options.common.base import AppiumOptions
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
|
||||
class LoginTest(unittest.TestCase):
|
||||
|
||||
mockServerProcess: subprocess.Popen
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
options = AppiumOptions()
|
||||
options.set_capability("app", "neochat --ignore-ssl-errors")
|
||||
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
|
||||
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
if not self._outcome.result.wasSuccessful():
|
||||
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
self.mockServerProcess.terminate()
|
||||
self.driver.quit()
|
||||
|
||||
def test_login(self):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
|
||||
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Matrix ID").send_keys("@user:localhost:1234")
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Continue").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Password").send_keys("1234")
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Login").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Join some rooms to get started").click()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
48
appiumtests/openuserdetailstest.py
Executable file
48
appiumtests/openuserdetailstest.py
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
|
||||
# SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from appium import webdriver
|
||||
from appium.options.common.base import AppiumOptions
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
|
||||
class OpenUserDetailsTest(unittest.TestCase):
|
||||
|
||||
mockServerProcess: subprocess.Popen
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.mockServerProcess = subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__), "login-server.py")])
|
||||
options = AppiumOptions()
|
||||
options.set_capability("app", "neochat --ignore-ssl-errors --test")
|
||||
cls.driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', options=options)
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
if not self._outcome.result.wasSuccessful():
|
||||
self.driver.get_screenshot_as_file("failed_test_shot_{}.png".format(self.id()))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(self):
|
||||
self.mockServerProcess.terminate()
|
||||
self.driver.quit()
|
||||
|
||||
def test_open_sheet(self):
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="@user:localhost:1234").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Empty room (!room_id_1234:localhost:1234)").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="A Display Name").click()
|
||||
self.driver.find_element(by=AppiumBy.NAME, value="Account Details")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
|
||||
|
||||
ecm_add_test(
|
||||
neochatroomtest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
@@ -20,3 +22,27 @@ ecm_add_test(
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME delegatesizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
mediasizehelpertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME mediasizehelpertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
eventhandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME eventhandlertest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
chatbarcachetest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME chatbarcachetest
|
||||
)
|
||||
|
||||
ecm_add_test(
|
||||
chatdocumenthandlertest.cpp
|
||||
LINK_LIBRARIES neochat Qt::Test
|
||||
TEST_NAME chatdocumenthandlertest
|
||||
)
|
||||
|
||||
157
autotests/chatbarcachetest.cpp
Normal file
157
autotests/chatbarcachetest.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include <Quotient/syncdata.h>
|
||||
#include <qtestcase.h>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class ChatBarCacheTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void empty();
|
||||
void noRoom();
|
||||
void badParent();
|
||||
void reply();
|
||||
void edit();
|
||||
void attachment();
|
||||
};
|
||||
|
||||
void ChatBarCacheTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
QFile testMinSyncFile;
|
||||
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
|
||||
testMinSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::empty()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
|
||||
QCOMPARE(chatBarCache->text(), QString());
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::noRoom()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache());
|
||||
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::badParent()
|
||||
{
|
||||
QScopedPointer<QObject> badParent(new QObject());
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(badParent.get()));
|
||||
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
|
||||
|
||||
// These should return empty even though a reply ID has been set because the
|
||||
// ChatBarCache has no parent.
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationUser(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.");
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::reply()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(QLatin1String("some text"));
|
||||
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
|
||||
chatBarCache->setReplyId(QLatin1String("$153456789:example.org"));
|
||||
|
||||
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
|
||||
QCOMPARE(chatBarCache->isReplying(), true);
|
||||
QCOMPARE(chatBarCache->replyId(), QLatin1String("$153456789:example.org"));
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
|
||||
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::edit()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(QLatin1String("some text"));
|
||||
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
|
||||
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
|
||||
|
||||
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), true);
|
||||
QCOMPARE(chatBarCache->editId(), QLatin1String("$153456789:example.org"));
|
||||
QCOMPARE(chatBarCache->relationUser(), room->getUser(room->user(QLatin1String("@example:example.org"))));
|
||||
QCOMPARE(chatBarCache->relationMessage(), QLatin1String("This is an example\ntext message"));
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QString());
|
||||
}
|
||||
|
||||
void ChatBarCacheTest::attachment()
|
||||
{
|
||||
QScopedPointer<ChatBarCache> chatBarCache(new ChatBarCache(room));
|
||||
chatBarCache->setText(QLatin1String("some text"));
|
||||
chatBarCache->setEditId(QLatin1String("$153456789:example.org"));
|
||||
chatBarCache->setAttachmentPath(QLatin1String("some/path"));
|
||||
|
||||
QCOMPARE(chatBarCache->text(), QLatin1String("some text"));
|
||||
QCOMPARE(chatBarCache->isReplying(), false);
|
||||
QCOMPARE(chatBarCache->replyId(), QString());
|
||||
QCOMPARE(chatBarCache->isEditing(), false);
|
||||
QCOMPARE(chatBarCache->editId(), QString());
|
||||
QCOMPARE(chatBarCache->relationUser(), room->getUser(nullptr));
|
||||
QCOMPARE(chatBarCache->relationMessage(), QString());
|
||||
QCOMPARE(chatBarCache->attachmentPath(), QLatin1String("some/path"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ChatBarCacheTest)
|
||||
#include "chatbarcachetest.moc"
|
||||
36
autotests/chatdocumenthandlertest.cpp
Normal file
36
autotests/chatdocumenthandlertest.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "chatdocumenthandler.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
class ChatDocumentHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
ChatDocumentHandler emptyHandler;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void nullComplete();
|
||||
};
|
||||
|
||||
void ChatDocumentHandlerTest::initTestCase()
|
||||
{
|
||||
// HACK: this is to stop KStatusNotifierItem SEGFAULTING on cleanup.
|
||||
NeoChatConfig::self()->setSystemTray(false);
|
||||
}
|
||||
|
||||
void ChatDocumentHandlerTest::nullComplete()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "complete called with m_document set to nullptr.");
|
||||
emptyHandler.complete(0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ChatDocumentHandlerTest)
|
||||
#include "chatdocumenthandlertest.moc"
|
||||
410
autotests/data/test-eventhandler-sync.json
Normal file
410
autotests/data/test-eventhandler-sync.json
Normal file
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$153456789:example.org": {
|
||||
"m.read": {
|
||||
"@alice:matrix.org": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@bob:example.com": {
|
||||
"ts": 1436451550453
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tim:example.com": {
|
||||
"ts": 1436451550454
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@jeff:example.com": {
|
||||
"ts": 1436451550455
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@tina:example.com": {
|
||||
"ts": 1436451550456
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@sally:example.com": {
|
||||
"ts": 1436451550457
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"$1532735824654:example.org": {
|
||||
"m.read": {
|
||||
"@fred:example.com": {
|
||||
"ts": 1436451550458
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.receipt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"displayname": "Look\nat\nme\nI\nput\nnewlines\nin\nmy\ndisplay name",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$143273582443PhrSh:example.org",
|
||||
"origin_server_ts": 1432735824659,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@newline:example.org",
|
||||
"state_key": "@newline:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 12345
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example\ntext message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://kde.org/123456",
|
||||
"displayname": "after",
|
||||
"membership": "join"
|
||||
},
|
||||
"origin_server_ts": 1690651134736,
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@example:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"replaces_state": "$1234567890:example.org",
|
||||
"prev_content": {
|
||||
"avatar_url": "mxc://kde.org/12345",
|
||||
"displayname": "before",
|
||||
"membership": "join"
|
||||
},
|
||||
"prev_sender": "@example:example.orgg",
|
||||
"age": 1234
|
||||
},
|
||||
"event_id": "$143273583553PhrSn:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "This is a highlight @bob:kde.org and this is a link https://kde.org",
|
||||
"format": "org.matrix.custom.html",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1532735824654:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1233
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$153456789:example.org",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"origin_server_ts": 1690322545182,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 390159120
|
||||
},
|
||||
"event_id": "$163456789:example.org",
|
||||
"age": 390159120
|
||||
},
|
||||
{
|
||||
"age": 4926305285,
|
||||
"content": {
|
||||
"body": "video caption",
|
||||
"filename": "video.mp4",
|
||||
"info": {
|
||||
"duration": 10,
|
||||
"h": 1080,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 62650636,
|
||||
"w": 1920,
|
||||
"thumbnail_info": {
|
||||
"h": 450,
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 382249,
|
||||
"w": 800
|
||||
},
|
||||
"thumbnail_url": "mxc://kde.org/2234567"
|
||||
},
|
||||
"msgtype": "m.video",
|
||||
"url": "mxc://kde.org/1234567"
|
||||
},
|
||||
"event_id": "$263456789:example.org",
|
||||
"origin_server_ts": 1685793783330,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 4926305285
|
||||
},
|
||||
"user_id": "@example:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> This is an example\ntext message\n\nreply",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!jEsUZKDJdhlrceRyVU:example.org/$153456789:example.org?via=kde.org&via=matrix.org\">In reply to</a> <a href=\"https://matrix.to/#/@example:example.org\">@example:example.org</a><br><b>This is an example<br>text message</b></blockquote></mx-reply>reply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$153456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965572,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456789:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "> <@example:example.org> video caption\n\nreply",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$263456789:example.org"
|
||||
}
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"origin_server_ts": 1690725965573,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 98
|
||||
},
|
||||
"event_id": "$154456799:example.org",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
|
||||
},
|
||||
{
|
||||
"age": 96845207,
|
||||
"content": {
|
||||
"body": "Lat: 51.7035, Lon: -1.14394",
|
||||
"geo_uri": "geo:51.7035,-1.14394",
|
||||
"msgtype": "m.location",
|
||||
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
|
||||
"org.matrix.msc3488.asset": {
|
||||
"type": "m.pin"
|
||||
},
|
||||
"org.matrix.msc3488.location": {
|
||||
"uri": "geo:51.7035,-1.14394"
|
||||
}
|
||||
},
|
||||
"event_id": "$1544567999:example.org",
|
||||
"origin_server_ts": 1690821582876,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 96845207
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Thread root",
|
||||
"format": "org.matrix.custom.html",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$threadroot:example.org",
|
||||
"origin_server_ts": 1690821582879,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Thread message 1",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.thread",
|
||||
"event_id": "$threadroot:example.org",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$threadroot:example.org"
|
||||
},
|
||||
"is_falling_back": true
|
||||
}
|
||||
},
|
||||
"event_id": "$threadmessage1:example.org",
|
||||
"origin_server_ts": 1690821582890,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1238
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "Thread message 2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.thread",
|
||||
"event_id": "$threadroot:example.org",
|
||||
"m.in_reply_to": {
|
||||
"event_id": "$threadmessage1:example.org"
|
||||
},
|
||||
"is_falling_back": true
|
||||
}
|
||||
},
|
||||
"event_id": "$threadmessage2:example.org",
|
||||
"origin_server_ts": 1690821582890,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1238
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "A message from someone who thought it was a good idea to put newlines in their display name.",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456889:example.org",
|
||||
"origin_server_ts": 14327358246589,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@newline:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1230
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
87
autotests/data/test-min-sync.json
Normal file
87
autotests/data/test-min-sync.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an example\ntext message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example<br>text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$153456789:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
124
autotests/data/test-texthandler-sync.json
Normal file
124
autotests/data/test-texthandler-sync.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an **example** text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "/me This is an emote.",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an emote.",
|
||||
"msgtype": "m.emote"
|
||||
},
|
||||
"event_id": "$153273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1231
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "tested",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
|
||||
"origin_server_ts": 1680948575928,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1747776,
|
||||
"m.relations": {
|
||||
"m.replace": {
|
||||
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
|
||||
"origin_server_ts": 1680948580992,
|
||||
"sender": "@example:example.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
}
|
||||
699
autotests/eventhandlertest.cpp
Normal file
699
autotests/eventhandlertest.cpp
Normal file
@@ -0,0 +1,699 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
|
||||
#include "eventhandler.h"
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
#include "enums/delegatetype.h"
|
||||
#include "linkpreviewer.h"
|
||||
#include "models/reactionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class TestRoom : public NeoChatRoom
|
||||
{
|
||||
public:
|
||||
using NeoChatRoom::NeoChatRoom;
|
||||
|
||||
void update(SyncRoomData &&data, bool fromCache = false)
|
||||
{
|
||||
Room::updateData(std::move(data), fromCache);
|
||||
}
|
||||
};
|
||||
|
||||
class EventHandlerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
Connection *connection = nullptr;
|
||||
TestRoom *room = nullptr;
|
||||
EventHandler eventHandler;
|
||||
EventHandler emptyHandler;
|
||||
EventHandler noEventHandler;
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
|
||||
void nullSetEvent();
|
||||
void eventId();
|
||||
void nullEventId();
|
||||
void delegateType_data();
|
||||
void delegateType();
|
||||
void nullDelegateType();
|
||||
void author();
|
||||
void nullAuthor();
|
||||
void authorDisplayName();
|
||||
void nullAuthorDisplayName();
|
||||
void singleLineSidplayName();
|
||||
void nullSingleLineDisplayName();
|
||||
void time();
|
||||
void nullTime();
|
||||
void timeString();
|
||||
void nullTimeString();
|
||||
void highlighted();
|
||||
void nullHighlighted();
|
||||
void hidden();
|
||||
void nullHidden();
|
||||
void body();
|
||||
void nullBody();
|
||||
void genericBody_data();
|
||||
void genericBody();
|
||||
void nullGenericBody();
|
||||
void mediaInfo();
|
||||
void nullMediaInfo();
|
||||
void linkPreviewer();
|
||||
void nullLinkPreviewer();
|
||||
void reactions();
|
||||
void nullReactions();
|
||||
void hasReply();
|
||||
void nullHasReply();
|
||||
void replyId();
|
||||
void nullReplyId();
|
||||
void replyDelegateType();
|
||||
void nullReplyDelegateType();
|
||||
void replyAuthor();
|
||||
void nullReplyAuthor();
|
||||
void replyBody();
|
||||
void nullReplyBody();
|
||||
void replyMediaInfo();
|
||||
void nullReplyMediaInfo();
|
||||
void thread();
|
||||
void nullThread();
|
||||
void location();
|
||||
void nullLocation();
|
||||
void readMarkers();
|
||||
void nullReadMarkers();
|
||||
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
void EventHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
QFile testEventHandlerSyncFile;
|
||||
testEventHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-eventhandler-sync.json"));
|
||||
testEventHandlerSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testEventHandlerSyncJson = QJsonDocument::fromJson(testEventHandlerSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testEventHandlerSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
|
||||
eventHandler.setRoom(room);
|
||||
noEventHandler.setRoom(room);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullSetEvent()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "cannot setEvent when m_room is set to nullptr.");
|
||||
emptyHandler.setEvent(room->messageEvents().at(0).get());
|
||||
}
|
||||
|
||||
void EventHandlerTest::eventId()
|
||||
{
|
||||
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||
|
||||
QCOMPARE(eventHandler.getId(), QStringLiteral("$153456789:example.org"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullEventId()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getId called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getId(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<DelegateType::Type>("delegateType");
|
||||
|
||||
QTest::newRow("message") << 0 << DelegateType::Message;
|
||||
QTest::newRow("state") << 1 << DelegateType::State;
|
||||
QTest::newRow("message 2") << 2 << DelegateType::Message;
|
||||
QTest::newRow("reaction") << 3 << DelegateType::Other;
|
||||
QTest::newRow("video") << 4 << DelegateType::Video;
|
||||
QTest::newRow("location") << 7 << DelegateType::Location;
|
||||
}
|
||||
|
||||
void EventHandlerTest::delegateType()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(DelegateType::Type, delegateType);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getDelegateType(), delegateType);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullDelegateType()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getDelegateType called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getDelegateType(), DelegateType::Other);
|
||||
}
|
||||
|
||||
void EventHandlerTest::author()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
auto author = room->user(event->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerAuthor = eventHandler.getAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerAuthor["isLocalUser"_ls], author->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerAuthor["id"_ls], author->id());
|
||||
QCOMPARE(eventHandlerAuthor["displayName"_ls], author->displayname(room));
|
||||
QCOMPARE(eventHandlerAuthor["avatarSource"_ls], room->avatarForMember(author));
|
||||
QCOMPARE(eventHandlerAuthor["avatarMediaId"_ls], author->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerAuthor["color"_ls], Utils::getUserColor(author->hueF()));
|
||||
QCOMPARE(eventHandlerAuthor["object"_ls], QVariant::fromValue(author));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullAuthor()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getAuthor(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthor called with m_event set to nullptr. Returning empty user.");
|
||||
QCOMPARE(noEventHandler.getAuthor(), room->getUser(nullptr));
|
||||
}
|
||||
|
||||
void EventHandlerTest::authorDisplayName()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getAuthorDisplayName(), QStringLiteral("before"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullAuthorDisplayName()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getAuthorDisplayName(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getAuthorDisplayName(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::singleLineSidplayName()
|
||||
{
|
||||
auto event = room->messageEvents().at(11).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.singleLineAuthorDisplayname(), QStringLiteral("Look at me I put newlines in my display name"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullSingleLineDisplayName()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.singleLineAuthorDisplayname(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getAuthorDisplayName called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.singleLineAuthorDisplayname(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::time()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getTime(), QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC));
|
||||
QCOMPARE(eventHandler.getTime(true, QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC)), QDateTime::fromMSecsSinceEpoch(1234, Qt::UTC));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullTime()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getTime called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getTime(), QDateTime());
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
|
||||
QCOMPARE(eventHandler.getTime(true), QDateTime());
|
||||
}
|
||||
|
||||
void EventHandlerTest::timeString()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
KFormat format;
|
||||
|
||||
QCOMPARE(eventHandler.getTimeString(false),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::ShortFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().time(), QLocale::LongFormat));
|
||||
QCOMPARE(eventHandler.getTimeString(true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC)),
|
||||
format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, Qt::UTC).toLocalTime().date(), QLocale::LongFormat));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullTimeString()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getTimeString called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getTimeString(false), QString());
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(0).get());
|
||||
QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event.");
|
||||
QCOMPARE(eventHandler.getTimeString(false, QLocale::ShortFormat, true), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::highlighted()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHighlighted(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullHighlighted()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.isHighlighted(), false);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "isHighlighted called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.isHighlighted(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hidden()
|
||||
{
|
||||
auto event = room->messageEvents().at(3).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isHidden(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullHidden()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.isHidden(), false);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "isHidden called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.isHidden(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::body()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullBody()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getRichBody called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getRichBody(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getPlainBody called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getPlainBody(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody_data()
|
||||
{
|
||||
QTest::addColumn<int>("eventNum");
|
||||
QTest::addColumn<QString>("output");
|
||||
|
||||
QTest::newRow("message") << 0 << QStringLiteral("sent a message");
|
||||
QTest::newRow("member") << 1 << QStringLiteral("changed their display name and updated their avatar");
|
||||
QTest::newRow("message 2") << 2 << QStringLiteral("sent a message");
|
||||
QTest::newRow("reaction") << 3 << QStringLiteral("Unknown event");
|
||||
QTest::newRow("video") << 4 << QStringLiteral("sent a message");
|
||||
}
|
||||
|
||||
void EventHandlerTest::genericBody()
|
||||
{
|
||||
QFETCH(int, eventNum);
|
||||
QFETCH(QString, output);
|
||||
|
||||
eventHandler.setEvent(room->messageEvents().at(eventNum).get());
|
||||
|
||||
QCOMPARE(eventHandler.getGenericBody(), output);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullGenericBody()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getGenericBody called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getGenericBody(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::mediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(event->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullMediaInfo()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getMediaInfo(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getMediaInfo called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getMediaInfo(), QVariantMap());
|
||||
}
|
||||
|
||||
void EventHandlerTest::linkPreviewer()
|
||||
{
|
||||
auto event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer()->url(), QUrl("https://kde.org"_ls));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLinkPreviewer(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullLinkPreviewer()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getLinkPreviewer(), nullptr);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLinkPreviewer called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getLinkPreviewer(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::reactions()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReactions()->rowCount(), 1);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReactions()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReactions(), nullptr);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReactions called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReactions(), nullptr);
|
||||
}
|
||||
|
||||
void EventHandlerTest::hasReply()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), true);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReply(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullHasReply()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "hasReply called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.hasReply(), false);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyId()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$153456789:example.org"));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral(""));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReplyId()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyId called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReplyId(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyDelegateType()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Message);
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyDelegateType(), DelegateType::Other);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReplyDelegateType()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReplyDelegateType(), DelegateType::Other);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyDelegateType called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReplyDelegateType(), DelegateType::Other);
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyAuthor()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
auto replyEvent = room->messageEvents().at(0).get();
|
||||
auto replyAuthor = room->user(replyEvent->senderId());
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto eventHandlerReplyAuthor = eventHandler.getReplyAuthor();
|
||||
|
||||
QCOMPARE(eventHandlerReplyAuthor["isLocalUser"_ls], replyAuthor->id() == room->localUser()->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["id"_ls], replyAuthor->id());
|
||||
QCOMPARE(eventHandlerReplyAuthor["displayName"_ls], replyAuthor->displayname(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarSource"_ls], room->avatarForMember(replyAuthor));
|
||||
QCOMPARE(eventHandlerReplyAuthor["avatarMediaId"_ls], replyAuthor->avatarMediaId(room));
|
||||
QCOMPARE(eventHandlerReplyAuthor["color"_ls], Utils::getUserColor(replyAuthor->hueF()));
|
||||
QCOMPARE(eventHandlerReplyAuthor["object"_ls], QVariant::fromValue(replyAuthor));
|
||||
|
||||
event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyAuthor(), room->getUser(nullptr));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReplyAuthor()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReplyAuthor(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyAuthor called with m_event set to nullptr. Returning empty user.");
|
||||
QCOMPARE(noEventHandler.getReplyAuthor(), room->getUser(nullptr));
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyBody()
|
||||
{
|
||||
auto event = room->messageEvents().at(5).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getReplyRichBody(), QStringLiteral("<b>This is an example<br>text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyRichBody(true), QStringLiteral("<b>This is an example text message</b>"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(), QStringLiteral("This is an example\ntext message"));
|
||||
QCOMPARE(eventHandler.getReplyPlainBody(true), QStringLiteral("This is an example text message"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReplyBody()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyRichBody called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReplyRichBody(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyPlainBody called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReplyPlainBody(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::replyMediaInfo()
|
||||
{
|
||||
auto event = room->messageEvents().at(6).get();
|
||||
auto replyEvent = room->messageEvents().at(4).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
auto mediaInfo = eventHandler.getReplyMediaInfo();
|
||||
auto thumbnailInfo = mediaInfo["tempInfo"_ls].toMap();
|
||||
|
||||
QCOMPARE(mediaInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/1234567"_ls)));
|
||||
QCOMPARE(mediaInfo["mimeType"_ls], QStringLiteral("video/mp4"));
|
||||
QCOMPARE(mediaInfo["mimeIcon"_ls], QStringLiteral("video-mp4"));
|
||||
QCOMPARE(mediaInfo["size"_ls], 62650636);
|
||||
QCOMPARE(mediaInfo["duration"_ls], 10);
|
||||
QCOMPARE(mediaInfo["width"_ls], 1920);
|
||||
QCOMPARE(mediaInfo["height"_ls], 1080);
|
||||
QCOMPARE(thumbnailInfo["source"_ls], room->makeMediaUrl(replyEvent->id(), QUrl("mxc://kde.org/2234567"_ls)));
|
||||
QCOMPARE(thumbnailInfo["mimeType"_ls], QStringLiteral("image/jpeg"));
|
||||
QCOMPARE(thumbnailInfo["mimeIcon"_ls], QStringLiteral("image-jpeg"));
|
||||
QCOMPARE(thumbnailInfo["size"_ls], 382249);
|
||||
QCOMPARE(thumbnailInfo["width"_ls], 800);
|
||||
QCOMPARE(thumbnailInfo["height"_ls], 450);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReplyMediaInfo()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReplyMediaInfo(), QVariantMap());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReplyMediaInfo called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReplyMediaInfo(), QVariantMap());
|
||||
}
|
||||
|
||||
void EventHandlerTest::thread()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isThreaded(), false);
|
||||
QCOMPARE(eventHandler.threadRoot(), QString());
|
||||
|
||||
event = room->messageEvents().at(9).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isThreaded(), true);
|
||||
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadroot:example.org"));
|
||||
|
||||
event = room->messageEvents().at(10).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.isThreaded(), true);
|
||||
QCOMPARE(eventHandler.threadRoot(), QStringLiteral("$threadroot:example.org"));
|
||||
QCOMPARE(eventHandler.getReplyId(), QStringLiteral("$threadmessage1:example.org"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullThread()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "isThreaded called with m_event set to nullptr.");
|
||||
QCOMPARE(emptyHandler.isThreaded(), false);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "threadRoot called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.threadRoot(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::location()
|
||||
{
|
||||
auto event = room->messageEvents().at(7).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.getLatitude(), QStringLiteral("51.7035").toFloat());
|
||||
QCOMPARE(eventHandler.getLongitude(), QStringLiteral("-1.14394").toFloat());
|
||||
QCOMPARE(eventHandler.getLocationAssetType(), QStringLiteral("m.pin"));
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullLocation()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLatitude called with m_event set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getLatitude(), -100.0);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLongitude called with m_event set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getLongitude(), -200.0);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getLocationAssetType called with m_event set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getLocationAssetType(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::readMarkers()
|
||||
{
|
||||
auto event = room->messageEvents().at(0).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
auto readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 1);
|
||||
QCOMPARE(readMarkers[0].toMap()["id"_ls], QStringLiteral("@alice:matrix.org"));
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QString());
|
||||
QCOMPARE(eventHandler.getReadMarkersString(), QStringLiteral("1 user: @alice:matrix.org"));
|
||||
|
||||
event = room->messageEvents().at(2).get();
|
||||
eventHandler.setEvent(event);
|
||||
|
||||
QCOMPARE(eventHandler.hasReadMarkers(), true);
|
||||
|
||||
readMarkers = eventHandler.getReadMarkers();
|
||||
|
||||
QCOMPARE(readMarkers.size(), 5);
|
||||
|
||||
QCOMPARE(eventHandler.getNumberExcessReadMarkers(), QStringLiteral("+ 1"));
|
||||
// There are no guarantees on the order of the users it will be different every time so don't match the whole string.
|
||||
QCOMPARE(eventHandler.getReadMarkersString().startsWith(QStringLiteral("6 users:")), true);
|
||||
}
|
||||
|
||||
void EventHandlerTest::nullReadMarkers()
|
||||
{
|
||||
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.hasReadMarkers(), false);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReadMarkers(), QVariantList());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getNumberExcessReadMarkers(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_room set to nullptr.");
|
||||
QCOMPARE(emptyHandler.getReadMarkersString(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "hasReadMarkers called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.hasReadMarkers(), false);
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReadMarkers called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReadMarkers(), QVariantList());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getNumberExcessReadMarkers called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getNumberExcessReadMarkers(), QString());
|
||||
|
||||
QTest::ignoreMessage(QtWarningMsg, "getReadMarkersString called with m_event set to nullptr.");
|
||||
QCOMPARE(noEventHandler.getReadMarkersString(), QString());
|
||||
}
|
||||
|
||||
void EventHandlerTest::cleanup()
|
||||
{
|
||||
eventHandler.setEvent(nullptr);
|
||||
}
|
||||
|
||||
QTEST_MAIN(EventHandlerTest)
|
||||
#include "eventhandlertest.moc"
|
||||
72
autotests/mediasizehelpertest.cpp
Normal file
72
autotests/mediasizehelpertest.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
#include <qglobal.h>
|
||||
#include <qtestcase.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "mediasizehelper.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
class MediaSizeHelperTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void uninitialized();
|
||||
|
||||
void limits_data();
|
||||
void limits();
|
||||
};
|
||||
|
||||
void MediaSizeHelperTest::uninitialized()
|
||||
{
|
||||
MediaSizeHelper mediasizehelper;
|
||||
QCOMPARE(mediasizehelper.currentSize(), QSize(540, qRound(qreal(NeoChatConfig::self()->mediaMaxWidth()) / qreal(16.0) * qreal(9.0))));
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("mediaWidth");
|
||||
QTest::addColumn<qreal>("mediaHeight");
|
||||
QTest::addColumn<qreal>("contentMaxWidth");
|
||||
QTest::addColumn<qreal>("contentMaxHeight");
|
||||
QTest::addColumn<QSize>("currentSize");
|
||||
|
||||
QTest::newRow("media smaller than content limits") << qreal(200) << qreal(150) << qreal(400) << qreal(900) << QSize(200, 150);
|
||||
QTest::newRow("media smaller than max limits") << qreal(200) << qreal(150) << qreal(-1) << qreal(-1) << QSize(200, 150);
|
||||
|
||||
QTest::newRow("limit by max width") << qreal(600) << qreal(50) << qreal(-1) << qreal(-1) << QSize(540, qRound(qreal(540) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by max height") << qreal(50) << qreal(600) << qreal(-1) << qreal(-1) << QSize(qRound(qreal(540) * (qreal(50) / qreal(600))), 540);
|
||||
|
||||
QTest::newRow("limit by content width") << qreal(600) << qreal(50) << qreal(300) << qreal(-1) << QSize(300, qRound(qreal(300) / (qreal(600) / qreal(50))));
|
||||
QTest::newRow("limit by content height") << qreal(50) << qreal(600) << qreal(-1) << qreal(300) << QSize(qRound(qreal(300) * (qreal(50) / qreal(600))), 300);
|
||||
|
||||
QTest::newRow("limit by content width tall media")
|
||||
<< qreal(400) << qreal(600) << qreal(100) << qreal(400) << QSize(100, qRound(qreal(100) / (qreal(400) / qreal(600))));
|
||||
QTest::newRow("limit by content height wide media")
|
||||
<< qreal(1000) << qreal(600) << qreal(400) << qreal(100) << QSize(qRound(qreal(100) * (qreal(1000) / qreal(600))), 100);
|
||||
}
|
||||
|
||||
void MediaSizeHelperTest::limits()
|
||||
{
|
||||
QFETCH(qreal, mediaWidth);
|
||||
QFETCH(qreal, mediaHeight);
|
||||
QFETCH(qreal, contentMaxWidth);
|
||||
QFETCH(qreal, contentMaxHeight);
|
||||
QFETCH(QSize, currentSize);
|
||||
|
||||
MediaSizeHelper mediasizehelper;
|
||||
mediasizehelper.setMediaWidth(mediaWidth);
|
||||
mediasizehelper.setMediaHeight(mediaHeight);
|
||||
mediasizehelper.setContentMaxWidth(contentMaxWidth);
|
||||
mediasizehelper.setContentMaxHeight(contentMaxHeight);
|
||||
|
||||
QCOMPARE(mediasizehelper.currentSize(), currentSize);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(MediaSizeHelperTest)
|
||||
#include "mediasizehelpertest.moc"
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
#include "neochatroom.h"
|
||||
|
||||
#include <connection.h>
|
||||
#include <quotient_common.h>
|
||||
#include <syncdata.h>
|
||||
#include <Quotient/connection.h>
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -42,101 +42,18 @@ void NeoChatRoomTest::initTestCase()
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
auto json = QJsonDocument::fromJson(R"EVENT({
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an **example** text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1235
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
})EVENT");
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
QFile testMinSyncFile;
|
||||
testMinSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-min-sync.json"));
|
||||
testMinSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testMinSyncJson = QJsonDocument::fromJson(testMinSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testMinSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::subtitleTextTest()
|
||||
{
|
||||
QCOMPARE(room->timelineSize(), 1);
|
||||
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example text message"));
|
||||
QCOMPARE(room->lastEventToString(), QStringLiteral("@example:example.org: This is an example\ntext message"));
|
||||
}
|
||||
|
||||
void NeoChatRoomTest::eventTest()
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
#include "texthandler.h"
|
||||
|
||||
#include <Quotient/quotient_common.h>
|
||||
#include <Quotient/syncdata.h>
|
||||
#include <qnamespace.h>
|
||||
#include <quotient_common.h>
|
||||
#include <syncdata.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -38,6 +40,7 @@ private Q_SLOTS:
|
||||
void stripDisallowedTags();
|
||||
void stripDisallowedAttributes();
|
||||
void emptyCodeTags();
|
||||
void formatBlockQuote();
|
||||
|
||||
void sendSimpleStringCase();
|
||||
void sendSingleParaMarkup();
|
||||
@@ -64,6 +67,7 @@ private Q_SLOTS:
|
||||
void receiveRichEdited_data();
|
||||
void receiveRichEdited();
|
||||
void receiveLineSeparator();
|
||||
void receiveRichCodeUrl();
|
||||
|
||||
void linkPreviewsMatch_data();
|
||||
void linkPreviewsMatch();
|
||||
@@ -71,140 +75,18 @@ private Q_SLOTS:
|
||||
void linkPreviewsReject();
|
||||
};
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::initTestCase()
|
||||
{
|
||||
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
|
||||
room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join);
|
||||
|
||||
const auto json = QJsonDocument::fromJson(R"EVENT({
|
||||
"account_data": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"custom_config_key": "custom_config_value"
|
||||
},
|
||||
"type": "org.example.custom.room.config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
|
||||
"displayname": "Alice Margatroid",
|
||||
"membership": "join",
|
||||
"reason": "Looking for support"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "@alice:example.org",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@alice:example.com",
|
||||
"@bob:example.com"
|
||||
],
|
||||
"m.invited_member_count": 0,
|
||||
"m.joined_member_count": 2
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "This is an **example** text message",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<b>This is an example text message</b>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1432735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1232
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "/me This is an emote.",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "This is an emote.",
|
||||
"msgtype": "m.emote"
|
||||
},
|
||||
"event_id": "$153273582443PhrSn:example.org",
|
||||
"origin_server_ts": 1532735824654,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1231
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"body": "tested",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E",
|
||||
"origin_server_ts": 1680948575928,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"sender": "@example:example.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 1747776,
|
||||
"m.relations": {
|
||||
"m.replace": {
|
||||
"event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ",
|
||||
"origin_server_ts": 1680948580992,
|
||||
"sender": "@example:example.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t34-23535_0_0"
|
||||
}
|
||||
})EVENT");
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object());
|
||||
QFile testTextHandlerSyncFile;
|
||||
testTextHandlerSyncFile.setFileName(QLatin1String(DATA_DIR) + u'/' + QLatin1String("test-texthandler-sync.json"));
|
||||
testTextHandlerSyncFile.open(QIODevice::ReadOnly);
|
||||
const auto testTextHandlerSyncJson = QJsonDocument::fromJson(testTextHandlerSyncFile.readAll());
|
||||
SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, testTextHandlerSyncJson.object());
|
||||
room->update(std::move(roomData));
|
||||
}
|
||||
#endif
|
||||
|
||||
void TextHandlerTest::allowedAttributes()
|
||||
{
|
||||
@@ -265,6 +147,16 @@ void TextHandlerTest::emptyCodeTags()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
void TextHandlerTest::formatBlockQuote()
|
||||
{
|
||||
auto input = QStringLiteral("<blockquote>\n<p>Lorem Ispum</p>\n</blockquote>");
|
||||
auto expectedOutput = QStringLiteral("<blockquote><table><tr><td>\u201CLorem Ispum\u201D</td></tr></table></blockquote>");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(input);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), expectedOutput);
|
||||
}
|
||||
|
||||
void TextHandlerTest::sendSimpleStringCase()
|
||||
{
|
||||
const QString testInputString = QStringLiteral("This data should just be put in a paragraph.");
|
||||
@@ -363,6 +255,7 @@ void TextHandlerTest::receiveRichInPlainOut_data()
|
||||
|
||||
QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b");
|
||||
QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\"");
|
||||
QTest::newRow("new line") << QStringLiteral("new<br>line") << QStringLiteral("new\nline");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichInPlainOut()
|
||||
@@ -479,7 +372,6 @@ void TextHandlerTest::receiveRichtextIn()
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString);
|
||||
}
|
||||
|
||||
#ifdef QUOTIENT_07
|
||||
void TextHandlerTest::receiveRichMxcUrl()
|
||||
{
|
||||
const QString testInputString = QStringLiteral(
|
||||
@@ -497,7 +389,6 @@ void TextHandlerTest::receiveRichMxcUrl()
|
||||
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* For when your rich input string has a plain text url left in.
|
||||
@@ -559,10 +450,10 @@ void TextHandlerTest::receiveRichPlainUrl()
|
||||
void TextHandlerTest::receiveRichEmote()
|
||||
{
|
||||
auto event = room->messageEvents().at(1).get();
|
||||
auto author = static_cast<NeoChatUser *>(room->user(event->senderId()));
|
||||
auto author = room->user(event->senderId());
|
||||
const QString testInputString = QStringLiteral("This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:") + author->color().name()
|
||||
+ QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
const QString testOutputString = QStringLiteral("* <a href=\"https://matrix.to/#/@example:example.org\" style=\"color:")
|
||||
+ Utils::getUserColor(author->hueF()).name() + QStringLiteral("\">@example:example.org</a> This is an emote.");
|
||||
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(testInputString);
|
||||
@@ -578,8 +469,9 @@ void TextHandlerTest::receiveRichEdited_data()
|
||||
QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited <span style=\"color:#000000\">(edited)</span>");
|
||||
QTest::newRow("multiple paragraphs") << QStringLiteral("<p>Edited</p>\n<p>Edited</p>")
|
||||
<< QStringLiteral("<p>Edited</p>\n<p>Edited <span style=\"color:#000000\">(edited)</span></p>");
|
||||
QTest::newRow("blockquote") << QStringLiteral("<blockquote>Edited</blockquote>")
|
||||
<< QStringLiteral("<blockquote>Edited</blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
|
||||
QTest::newRow("blockquote")
|
||||
<< QStringLiteral("<blockquote>Edited</blockquote>")
|
||||
<< QStringLiteral("<blockquote><table><tr><td>\u201CEdited\u201D</td></tr></table></blockquote><p> <span style=\"color:#000000\">(edited)</span></p>");
|
||||
}
|
||||
|
||||
void TextHandlerTest::receiveRichEdited()
|
||||
@@ -606,13 +498,13 @@ void TextHandlerTest::linkPreviewsMatch_data()
|
||||
QTest::addColumn<QString>("testInputString");
|
||||
QTest::addColumn<QList<QUrl>>("testOutputLinks");
|
||||
|
||||
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org")});
|
||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org")});
|
||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org")});
|
||||
QTest::newRow("plainHttps") << QStringLiteral("https://kde.org") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
||||
QTest::newRow("richHttps") << QStringLiteral("<a href=\"https://kde.org\">Rich Link</a>") << QList<QUrl>({QUrl("https://kde.org"_ls)});
|
||||
QTest::newRow("plainWww") << QStringLiteral("www.example.org") << QList<QUrl>({QUrl("www.example.org"_ls)});
|
||||
QTest::newRow("multipleHttps") << QStringLiteral("https://kde.org www.example.org")
|
||||
<< QList<QUrl>({
|
||||
QUrl("https://kde.org"),
|
||||
QUrl("www.example.org"),
|
||||
QUrl("https://kde.org"_ls),
|
||||
QUrl("www.example.org"_ls),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -648,5 +540,13 @@ void TextHandlerTest::linkPreviewsReject()
|
||||
QCOMPARE(testTextHandler.getLinkPreviews(), testOutputLinks);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TextHandlerTest)
|
||||
void TextHandlerTest::receiveRichCodeUrl()
|
||||
{
|
||||
auto input = QStringLiteral("<code>https://kde.org</code>");
|
||||
TextHandler testTextHandler;
|
||||
testTextHandler.setData(input);
|
||||
QCOMPARE(testTextHandler.handleRecieveRichText(), input);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TextHandlerTest)
|
||||
#include "texthandlertest.moc"
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${MAN_INSTALL_DIR})
|
||||
kdoctools_create_manpage(man-neochat.1.docbook 1 INSTALL_DESTINATION ${KDE_INSTALL_MANDIR})
|
||||
|
||||
BIN
icons/300-apps-neochat.png
Normal file
BIN
icons/300-apps-neochat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
icons/windows/promoimage-1920x1080.png
Normal file
BIN
icons/windows/promoimage-1920x1080.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
icons/windows/storelogo-1080x1080.png
Normal file
BIN
icons/windows/storelogo-1080x1080.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
icons/windows/storelogo-720x1080.png
Normal file
BIN
icons/windows/storelogo-720x1080.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -11,6 +11,7 @@
|
||||
</provides>
|
||||
<name>NeoChat</name>
|
||||
<name xml:lang="ar">نيوتشات</name>
|
||||
<name xml:lang="ast">NeoChat</name>
|
||||
<name xml:lang="az">NeoChat</name>
|
||||
<name xml:lang="ca">NeoChat</name>
|
||||
<name xml:lang="ca-valencia">NeoChat</name>
|
||||
@@ -18,6 +19,7 @@
|
||||
<name xml:lang="de">NeoChat</name>
|
||||
<name xml:lang="el">NeoChat</name>
|
||||
<name xml:lang="en-GB">NeoChat</name>
|
||||
<name xml:lang="eo">NeoChat</name>
|
||||
<name xml:lang="es">NeoChat</name>
|
||||
<name xml:lang="eu">NeoChat</name>
|
||||
<name xml:lang="fi">NeoChat</name>
|
||||
@@ -45,42 +47,29 @@
|
||||
<name xml:lang="uk">NeoChat</name>
|
||||
<name xml:lang="x-test">xxNeoChatxx</name>
|
||||
<name xml:lang="zh-CN">NeoChat</name>
|
||||
<summary>A client for matrix, the decentralized communication protocol</summary>
|
||||
<summary xml:lang="ar">عميل لماتركس، ميفاق الاتصال اللامركزي</summary>
|
||||
<summary xml:lang="az">Matrix üçün müştəri, mərkəzləşməmiş kommunikasiya protokolu</summary>
|
||||
<summary xml:lang="ca">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
|
||||
<summary xml:lang="ca-valencia">Un client de Matrix, el protocol de comunicacions descentralitzat</summary>
|
||||
<summary xml:lang="cs">Klient pro decentralizovaný komunikační protokol matrix</summary>
|
||||
<summary xml:lang="de">Ein Programm für Matrix, das dezentrale Kommunikationsprotokoll</summary>
|
||||
<summary xml:lang="el">Ένας πελάτης για το Matrix, το αποκεντρωμένο πρωτόκολλο επικοινωνίας</summary>
|
||||
<summary xml:lang="en-GB">A client for matrix, the decentralised communication protocol</summary>
|
||||
<summary xml:lang="es">Un cliente para Matrix, el protocolo de comunicaciones descentralizado</summary>
|
||||
<summary xml:lang="eu">Matrix, deszentralizatutako komunikazio protokolorako bezero bat</summary>
|
||||
<summary xml:lang="fi">Asiakas Matrixille, hajautetulle viestintäyhteyskäytännölle</summary>
|
||||
<summary xml:lang="fr">Un client pour « Matrix », le protocole décentralisé de communications.</summary>
|
||||
<summary xml:lang="gl">Un cliente para Matrix, o protocolo de comunicación descentralizada</summary>
|
||||
<summary xml:lang="hu">Kliens a matrixhoz, a decentralizált kommunikációs protokollhoz</summary>
|
||||
<summary xml:lang="ia">Un cliente per Matrix, le protocollo de communication decentralisate</summary>
|
||||
<summary xml:lang="id">Klien untuk matrix, protokol komunikasi terdesentralisasi</summary>
|
||||
<summary xml:lang="ie">Un cliente de Matrix, li protocol de communication decentralisat</summary>
|
||||
<summary xml:lang="it">Un client per matrix, il protocollo di comunicazione decentralizzato</summary>
|
||||
<summary xml:lang="ka">კლიენტი Matrix-სთვის, დეცენტრალიზებული კომუნიკაციის პროტოკოლისთვის</summary>
|
||||
<summary xml:lang="ko">Matrix, 분산 대화 프로토콜 클라이언트</summary>
|
||||
<summary xml:lang="nl">Een client voor matrix, het gedecentraliseerde communicatieprotocol</summary>
|
||||
<summary xml:lang="nn">Ein klient for Matrix, den desentraliserte lynmeldingsprotokollen</summary>
|
||||
<summary xml:lang="pa">ਮੈਟਰਿਕਸ, ਸਰਬ-ਸਾਂਝੇ ਸੰਚਾਰ ਪਰੋਟੋਕਾਲ, ਲਈ ਕਲਾਈਂਟ ਹੈ</summary>
|
||||
<summary xml:lang="pl">Program do obsługi matriksa, rozproszonego protokołu porozumiewania się</summary>
|
||||
<summary xml:lang="pt">Um cliente para o Matrix, o protocolo de comunicação descentralizado</summary>
|
||||
<summary xml:lang="pt-BR">Um cliente do Matrix, o protocolo de comunicação descentralizado</summary>
|
||||
<summary xml:lang="ru">Клиент для Matrix — децентрализованного коммуникационного протокола</summary>
|
||||
<summary xml:lang="sk">Klient pre matrix, decentralizovaný komunikačný protokol</summary>
|
||||
<summary xml:lang="sl">Odjemalec za matrix, decentralizirani komunikacijski protokol</summary>
|
||||
<summary xml:lang="sv">En klient för Matrix, det decentraliserade kommunikationsprotokollet</summary>
|
||||
<summary xml:lang="ta">மையமில்லா தகவல் பரிமாற்ற நெறிமுறையான மேட்ரிக்ஸுக்கான செயலி</summary>
|
||||
<summary xml:lang="tr">Merkezi olmayan iletişim protokolü Matrix için bir istemci</summary>
|
||||
<summary xml:lang="uk">Клієнт matrix, децентралізованого протоколу обміну даними</summary>
|
||||
<summary xml:lang="x-test">xxA client for matrix, the decentralized communication protocolxx</summary>
|
||||
<summary xml:lang="zh-CN">分布式通讯协议 Matrix 的客户端</summary>
|
||||
<summary>Chat with your friends on matrix</summary>
|
||||
<summary xml:lang="ar">دردش مع أصدقائك على ماتركس</summary>
|
||||
<summary xml:lang="ca">Xategeu amb els vostres amics a Matrix</summary>
|
||||
<summary xml:lang="ca-valencia">Xategeu amb els vostres amics a Matrix</summary>
|
||||
<summary xml:lang="eo">Babilu kun viaj amikoj sur matrix</summary>
|
||||
<summary xml:lang="es">Charle con sus amigos en matrix</summary>
|
||||
<summary xml:lang="eu">Berriketan jardun zure lagunekin «Matrix»en</summary>
|
||||
<summary xml:lang="fi">Keskustelu ystäviesi kanssa Matrixissa</summary>
|
||||
<summary xml:lang="fr">Discuter avec vos ami(e)s sur le réseau Matrix</summary>
|
||||
<summary xml:lang="gl">Charle coas súas amizades en Matrix.</summary>
|
||||
<summary xml:lang="ia">Starta Conversation conntu amicos sur matrix</summary>
|
||||
<summary xml:lang="it">Conversa con i tuoi contatti su matrix</summary>
|
||||
<summary xml:lang="ka">ესაუბრეთ მეგობრებს Matrix-ზე</summary>
|
||||
<summary xml:lang="ko">Matrix를 사용하여 친구들과 대화하기</summary>
|
||||
<summary xml:lang="nl">Met uw vrienden chatten op matrix</summary>
|
||||
<summary xml:lang="nn">Prat med vennar på Matrix</summary>
|
||||
<summary xml:lang="pl">Rozmawiaj ze swoimi znajomymi w Matriksie</summary>
|
||||
<summary xml:lang="sl">Klepet z vašimi prijatelji na matrixu</summary>
|
||||
<summary xml:lang="sv">Chatta med dina vänner på Matrix</summary>
|
||||
<summary xml:lang="ta">மேட்ரிக்ஸு மூலம் உங்கள் நண்பர்களிடம் பேசலாம்</summary>
|
||||
<summary xml:lang="tr">Matrix'te arkadaşlarınızla sohbet edin</summary>
|
||||
<summary xml:lang="uk">Спілкуйтеся з вашими друзями у matrix</summary>
|
||||
<summary xml:lang="x-test">xxChat with your friends on matrixxx</summary>
|
||||
<description>
|
||||
<p>NeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami
|
||||
to provide a convergent experience across multiple platforms.</p>
|
||||
@@ -88,34 +77,46 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="ca">El NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Fa servir els Frameworks de KDE i, sobretot, el Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat és un client de Matrix, el protocol descentralitzat de comunicacions de missatgeria instantània. Permet enviar missatges de text, fitxers de vídeo i d'àudio a la família, col·legues i amics. Utilitza els Frameworks de KDE i, sobretot, Kirigami per a proporcionar una experiència convergent a través de diverses plataformes.</p>
|
||||
<p xml:lang="en-GB">NeoChat is a client for Matrix, the decentralised communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="eo">NeoChat estas kliento por Matrix, la malcentra komunikoprotokolo por tuja mesaĝado. Ĝi ebligas al vi sendi tekstmesaĝojn, filmetojn kaj sondosierojn al via familio, kolegoj kaj amikoj. Ĝi uzas KDE-framojn kaj precipe Kirigami por disponigi konverĝan sperton tra pluraj platformoj.</p>
|
||||
<p xml:lang="es">NeoChat es un cliente para Matrix, el protocolo de comunicaciones descentralizado para mensajería instantánea. Le permite enviar mensajes de texto, vídeos y archivos de sonido a su familia, compañeros de trabajo y amigos. Usa la infraestructura de KDE y, en particular, Kirigami para proporcionar una experiencia convergente en muchas plataformas.</p>
|
||||
<p xml:lang="eu">NeoChat «Matrix»erako, bat-bateko mezularitzarako komunikazio deszentralizatuko protokolorako, bezero bat da. Zure sendiari, kide eta lagunei testu mezuak, bideo eta audio fitxategiak bidaltzeko aukera ematen dizu. «KDE Frameworks» eta bereziki «Kirigami» erabiltzen ditu plataforma anitzen artean esperientzia konbergente bat eskaintzeko.</p>
|
||||
<p xml:lang="fi">NeoChat on asiakassovellus Matrixille, hajautetulle pikaviestinyhteyskäytännölle. Sillä voi lähettää teksti-, video- ja ääniviestejä perheelle, tutuille ja ystäville. Se käyttää KDE-kehystä ja erityisesti Kirigamia tuottaakseen mukautuvan monialustaisen käyttökokemuksen.</p>
|
||||
<p xml:lang="fr">NeoChat est un client pour le protocole Matrix, un protocole décentralisé de communications pour messagerie instantané. Il vous permet d'envoyer des messages de texte, des vidéos et des fichiers audio à votre famille, vos collègues et vos amis. Il utilise les environnements de développement et plus précisément Kirigami pour fournir une expérience convergente sur plusieurs plate-formes. </p>
|
||||
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxería instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
|
||||
<p xml:lang="gl">NeoChat é un cliente para Matrix, o protocolo de comunicación descentralizada para mensaxaría instantánea. Podes enviar mensaxes de texto, vídeos e ficheiros de son á túa familia, colegas e amizades. Usas infraestruturas de KDE e principalmente Kirigami para proporcionar unha experiencia de uso converxente para varias plataformas.</p>
|
||||
<p xml:lang="ia">NeoChat es un cliente per Matrix, le protocollo de communication decentralisate per messager instantanee. Illo te permitte inviar messager de texto, files de video e audio a tu familia, collegas e amicos usante. Illo usa KDE frameworks e super toto Kirigamii forni un experientia convergente trans platteforme multiple.</p>
|
||||
<p xml:lang="it">NeoChat è un client per Matrix, il protocollo di comunicazione decentralizzato per la messaggistica istantanea. Ti consente di inviare messaggi di testo, video e file audio a familiari, colleghi e amici. Utilizza i framework KDE e in particolare Kirigami per fornire un'esperienza convergente su più piattaforme.</p>
|
||||
<p xml:lang="ka">NeoChat არის Matrix კლიენტი. ის საშუალებას გაძლევთ გაგზავნოთ ტექსტური შეტყობინებები, ვიდეოები და აუდიო ფაილები თქვენს ოჯახს, კოლეგებსა და მეგობრებს მატრიქსის პროტოკოლის გამოყენებით.</p>
|
||||
<p xml:lang="ko">NeoChat은 분산형 인스턴트 메시징 통신 프로토콜인 Matrix 클라이언트입니다. 가족, 동료, 친구에게 텍스트 메시지, 동영상, 오디오 파일을 전송할 수 있습니다. KDE 프레임워크와 Kirigami를 사용하여 다양한 플랫폼에서 일관적인 사용자 경험을 제공합니다.</p>
|
||||
<p xml:lang="nl">NeoChat is een client voor Matrix, het gedecentraliseerde communicatieprotocol voor instant messages. Het biedt u het verzenden van tekstberichten, video's en geluidsbestanden naar uw familie, collega's en vrienden. Het gebruik KDE frameworks en het meest opmerkelijk Kirigami om een convergente ervaring te leveren op meerdere platforms.</p>
|
||||
<p xml:lang="nn">NeoChat er ein klient for Matrix, ein protokoll for desentralisert kommunikasjon. Du kan utveksla tekst, lyd og videoar med kollegaar, vennar og familie. Programmet brukar KDE Frameworks og Kirigami for å gje ei brukarflate tilpassa ulike plattformer.</p>
|
||||
<p xml:lang="pl">NeoChat jest programem do Matriksa, protokołu rozproszonego porozumiewania się w czasie rzeczywistym. Umożliwia wysyłanie wiadomości tekstowych, filmów oraz dźwięku do twojej rodziny, znajomych oraz przyjaciół. Używa szkieletów KDE i głównie Kirigami, aby zapewnić spójne wrażenia na wielu platformach</p>
|
||||
<p xml:lang="pt">O NeoChat é um cliente do Matrix. O mesmo permite-lhe enviar mensagens de texto, ficheiros de vídeo e áudio para a sua família, colegas e amigos com o protocolo Matrix. Usa as plataformas do KDE, e principalmente o Kirigami, para oferecer uma experiência convergente entre várias plataformas.</p>
|
||||
<p xml:lang="sl">Neochat je odjemalec za Matrix, decentralizirani komunikacijski protokol za takojšnje sporočanje. Omogoča vam pošiljanje besedilnih sporočil, videoposnetkov in zvočnih datotek svoji družini, sodelavcem in prijateljem. Uporablja okvire ogrodje KDE frameworks in predvsem Kirigami za zagotavljanje konvergentne izkušnje na več platformah.</p>
|
||||
<p xml:lang="sv">NeoChat är en klient för Matrix, det decentraliserade kommunikationsprotokollet för direktmeddelanden. Den låter dig skicka textmeddelanden, videor och ljudfiler till din familj, kollegor och vänner. Den använder KDE Ramverk, i synnerhet Kirigami, för att tillhandahålla en konvergent upplevelse på flera plattformar.</p>
|
||||
<p xml:lang="tr">NeoChat, anlık iletileşme için merkezi olmayan iletişim protokolü olan Matrix için bir istemcidir. Ailenize, iş arkadaşlarınıza ve arkadaşlarınıza metin iletiler, videolar ve ses dosyaları göndermenize olanak tanır. Birden çok platformda yakınsak bir deneyim sağlamak için KDE Frameworks ve en önemlilerinden Kirigami'yi kullanır.</p>
|
||||
<p xml:lang="uk">NeoChat — клієнт Matrix, децентралізованого протоколу спілкування для миттєвого обміну повідомленнями. За його допомогою ви можете надсилати текстові повідомлення, відео та звукові файли вашій родин, колегами та друзям. У програмі використано бібліотеки KDE, зокрема Kirigami, для надання однорідного середовища на декількох програмних та апаратних платформах.</p>
|
||||
<p xml:lang="x-test">xxNeoChat is a client for Matrix, the decentralized communication protocol for instant messaging. It allows you to send text messages, videos and audio files to your family, colleagues and friends. It uses KDE frameworks and most notably Kirigami to provide a convergent experience across multiple platforms.xx</p>
|
||||
<p>NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
|
||||
<p xml:lang="ar">يهدف نيوتشات إلى أن يكون تطبيقًا كامل الميزات لمواصفات ماتركس. على هذا النحو يتم دعم كل شيء في المواصفات المستقرة الحالية مع الاستثناءات الملحوظة لـ VoIP والخيوط وبعض جوانب التشفير من طرف إلى طرف. هناك عدد قليل من الإغفالات الصغيرة الأخرى بسبب حقيقة أن مواصفات ماتركس تتطور باستمرار ، ولكن يبقى الهدف توفير الدعم النهائي للمواصفات بأكملها.</p>
|
||||
<p xml:lang="ca">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptatge d'extrem a extrem. Hi ha algunes altres omissions més petites a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu segueix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="ca-valencia">NeoChat pretén ser una aplicació amb totes les característiques per a l'especificació de Matrix. Com a tal, s'ha implementat tota l'especificació actual estable amb les notables excepcions de la VoIP, fils i alguns aspectes de l'encriptació d'extrem a extrem. Hi ha algunes altres omissions més xicotetes a causa del fet que l'especificació de Matrix està evolucionant constantment, però l'objectiu seguix sent proporcionar suport eventual per a tota l'especificació.</p>
|
||||
<p xml:lang="en-GB">NeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.</p>
|
||||
<p xml:lang="eo">NeoChat celas esti plene kapabla aplikaĵo por la Matrix-specifo. Kiel tia, ĉio en la nuna stabila specifo kun la rimarkindaj esceptoj de VoIP, fadenoj kaj kelkaj aspektoj de Fin-al-Fina Ĉifrado estas subtenataj. Estas kelkaj aliaj pli malgrandaj preterlasoj pro la fakto, ke la Matrix-speco konstante evoluas, sed la celo restas provizi finfine subtenon por la tuta specifaĵo.</p>
|
||||
<p xml:lang="es">NeoChat pretende ser una aplicación con todas las funciones para la especificación de Matrix. Como tal, admite todo en la especificación estable actual, con las notables excepciones de VoIP, subprocesos y algunas funciones de cifrado de extremo a extremo. Existen algunas omisiones menos importantes debido al hecho de que la especificación de Matrix está en constante evolución, pero el objetivo sigue siendo brindar compatibilidad final con toda la especificación.</p>
|
||||
<p xml:lang="eu">«NeoChat»ek «Matrix» zehaztapenaren ezaugarri guztiak eskaintzen dituen aplikazio bat izan nahi du. Beraz, egungo zehaztapen egonkorrean dagoen guztiaren euskarria du, VoIP, hariak eta muturren artean zifratzeko salbuespen nabarmenekin. Badira beste ez-betetze txikiago batzuk, «Matrix»en zehaztapena etengabe eboluzioan dagoelako, baina azken helburua zehaztapen osoaren euskarria ematea izaten jarraitzen du.</p>
|
||||
<p xml:lang="fi">NeoChat pyrkii olemaan Matrix-määritelmän täysominaisuuksinen sovellus, joten se tukee kaikkea nykyisessä vakaassa määritelmässä muutamaa huomattavaa poikkeusta lukuun ottamatta (VoIP, säikeet ja jotkin piirteet päästä päähän -salauksessa). Joitakin pienempiäkin puutteita on Matrix-määritelmän jatkuvan kehityksen vuoksi, mutta lopputavoitteena on tarjota määritelmän täysi tuki.</p>
|
||||
<p xml:lang="fr">L'objectif de NeoChat est d'être une application complète pour le protocole Matrix. En tant que tel, tout dans la spécification stable actuelle avec les exceptions notables de VoIP, les processus et certains aspects du chiffrement de bout en bout sont pris en charge. Il y a quelques autres petites omissions en raison du fait que la spécification du protocole Matrix est en constante évolution. Cependant, l'objectif reste de fournir un soutien éventuel pour l'ensemble de la spécification.</p>
|
||||
<p xml:lang="gl">NeoChat pretende ser unha aplicación completa para a especificación de Matrix. Coas excepcións de VoIP, conversas fiadas e algúns aspectos da cifraxe de extremo a extremo, a versión estábel segue as especificacións. Existen algunhas outras pequenas omisións debido ao feito de que Matrix está en continua evolución pero a intención é implementar a especificación completa.</p>
|
||||
<p xml:lang="ia">NeoChat aspira a esser un application plenemente eminente per le specification de Matrix. Tal como omne cosas in le specification currentemente stabile con le exceptiones notabile de VOIP, threads e alcun aspectos del cryptation End-to-End es supportate. Il ha ltere pauc omissiones, debite al facto que le specification de Matrix es in evolution constante ma le aspiration remane a fornir supporto eventual per le integre specification.</p>
|
||||
<p xml:lang="it">NeoChat mira ad essere un'applicazione completa per le specifiche Matrix. Pertanto, sono supportati tutti gli elementi dell'attuale specifica stabile con le notevoli eccezioni di VoIP, conversazioni e alcuni aspetti della cifratura end-to-end. Ci sono alcune altre piccole omissioni dovute al fatto che le specifiche Matrix sono in continua evoluzione, ma l'obiettivo rimane quello di fornire un eventuale supporto per l'intera specifica.</p>
|
||||
<p xml:lang="ka">NeoChat-ი მიზნად ისახავს Matrix სპეციფიკაციის სრული განხორციელება ჰქონდეს. როგორც ასეთი, ყველაფერი მიმდინარე სპეციფიკაციიდან, VoIP-ის, ძაფებისა და გამჭოლი დაშიფვრის ზოგიერთი ასპექტის გარდა, მხარდაჭერილია. შეძლება ასევე იყოს მცირე ლაფსუსებიც იმის გამო, რომ Matrix-ის სპეციფიკაცია მუდმივად ვითარგდება, მაგრამ ჩვენი მიზანი მისი სრული მხარდაჭერაა.</p>
|
||||
<p xml:lang="ko">NeoChat은 Matrix 표준을 따르는 프로그램을 목표로 합니다. 현재 안정 버전의 표준에서 제공하는 기능의 대부분을 지원하며, VoIP, 스레드, 일부 종단간 암호화와 같은 기능은 아직 지원하지 않습니다. Matrix 표준은 계속하여 진화 중이기 때문에 일부 기능이 빠져 있을 수도 있지만 장기적으로는 전체 표준을 지원하는 것이 목표입니다.</p>
|
||||
<p xml:lang="nl">NeoChat richt zich op het volledig bieden van alle mogelijkheden van de Matrix-specificatie. Alles in de huidige stabiele specificatie met merkbare uitzondering van VoIP, gekoppelde discussies en sommige aspecten van eind-tot-eind versleuteling worden ondersteund. Er zijn een paar andere kleinere omissies vanwege het feit dat de Matrix specificatie constant evolueert maar het doel blijft het eventueel bieden van ondersteuning van de gehele specificatie.</p>
|
||||
<p xml:lang="nn">NeoChat har som mål å støtta all funksjonalitet i Matrix-spesifikasjonen. Førebels er alt i den gjeldande stabile spesifikasjonen støtta, med unntak av VoIP, trådar og nokre delar av ende-til-kryptering. Det finst òg andre småting som ikkje er støtta, sidan Matrix-spesifikasjon er i stadig endring, men målet er altså støtte for alt.</p>
|
||||
<p xml:lang="pl">NeoChat w zamyśle ma być pełnowartościową aplikacją wg wytycznych Matriksa. Z tego powodu, wszystko, co jest obecnie w stabilnych wytycznych z pominięciem VoIP, wątków i niektórych części szyfrowania Użytkownik-do-Użytkownika są obecnie obsługiwane. Pominięto też kilka mniejszych rzeczy ze względu na ciągły rozwój wytycznych Matriksa, lecz celem nadal jest zapewnienie obsługi wszystkich wytycznych.</p>
|
||||
<p xml:lang="pt">O NeoChat pretende ser uma aplicação completa para a especificação do Matrix. Como tal, tudo o que existe na especificação estável actual, com as notáveis excepções do VoIP, tópicos e alguns aspectos da Encriptação Ponto-a-Ponto, são suportados. Existem mais algumas omissões, devido ao facto que a norma do Matrix está em constante evolução, mas o objectivo continua a ser oferecer o suporte eventual para a norma por inteiro.</p>
|
||||
<p xml:lang="sl">Neochat cilja, da bi bila popolna aplikacija po specifikaciji Matrixa. Kot takšna vsebuje vse v trenutni stabilni specifikaciji z pomembnimi izjemami pri VoIP, nitih in nekaterih vidikov šifriranja od konca do konca. Obstaja nekaj drugih manjših opustitev zaradi dejstva, da se specifikacija Matrix nenehno razvija, vendar cilj ostaja zagotoviti morebitno podporo celotni specifikaciji.</p>
|
||||
<p xml:lang="sv">NeoChat har som mål att vara ett fullständigt program enligt Matrix-specifikationen. Som sådant stöds allt i den nuvarande stabila specifikationen, med de nämnvärda undantagen VoIP, trådar och några aspekter av kryptering hela vägen. Det finns några ytterligare utelämnanden på grund av att Matrix-specifikationen hela tiden utvecklas, men målet förblir att till slut erbjuda stöd för hela specifikationen.</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi için tam özellikli bir uygulama olmayı hedefler. Bu nedenle; VoIP, ileti zincirleri ve Uçtan Uca Şifreleme'nin bazı yönleri gibi dikkate değer istisnalar dışında var olan kararlı belirtimdeki her şey desteklenir. Matrix belirtiminin sürekli gelişmesi nedeniyle birkaç küçük eksiklik daha var; ancak amaç tüm belirtim için nihai destek sağlamak olmayı sürdürüyor.</p>
|
||||
<p xml:lang="uk">Метою створення NeoChat є повноцінна реалізація програми для специфікації Matrix. Як наслідок, реалізовано усе у поточній стабільній специфікації, окрім голосового інтернет-зв'язку, потоків та деяких аспектів міжвузлового шифрування. Є також декілька інших незначних прогалин через те, що специфікація Matrix постійно змінюється, але метою лишається повна підтримка специфікації.</p>
|
||||
<p xml:lang="x-test">xxNeoChat aims to be a fully featured application for the Matrix specification. As such everything in the current stable specification with the notable exceptions of VoIP, threads and some aspects of End-to-End Encryption are supported. There are a few other smaller omissions due to the fact that the Matrix spec is constantly evolving but the aim remains to provide eventual support for the entire spec.xx</p>
|
||||
@@ -124,16 +125,23 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<p xml:lang="ca">A causa de la naturalesa del desenvolupament de l'especificació de Matrix, el NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="ca-valencia">A causa de la naturalea del desenvolupament de l'especificació de Matrix, NeoChat també implementa nombroses característiques inestables. Actualment són:</p>
|
||||
<p xml:lang="en-GB">Due to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:</p>
|
||||
<p xml:lang="eo">Pro la naturo de la Matrix-specifevoluo NeoChat ankaŭ subtenas multajn malstabilajn funkciojn. Nuntempe ĉi tiuj estas:</p>
|
||||
<p xml:lang="es">Debido a la naturaleza del desarrollo de la especificación de Matrix, NeoChat también permite numerosas funciones no estables, como:</p>
|
||||
<p xml:lang="eu">«Matrix» zehaztapenaren garapenaren izaeragatik, «Neo¢hat»ek ezaugarri ezegonkor ugari ere onartzen ditu. Gaur egun hauek:</p>
|
||||
<p xml:lang="fi">Matrix-määritelmän kehittyessä NeoChat tukee myös monia epävakaita ominaisuuksia. Tällä hetkellä näitä ovat:</p>
|
||||
<p xml:lang="fr">En raison de la nature du développement des spécifications du protocole Matrix, NeoChat prend également en charge de nombreuses fonctionnalités instables. Actuellement, ce sont :</p>
|
||||
<p xml:lang="gl">Debido á natureza do desenvolvemento da especificación de Matrix, NeoChat tamén inclúe varias funcionalidades non estábeis:</p>
|
||||
<p xml:lang="ia">Debite al natura del disveloppamento de specification de Matrix NeoChat tamben supporta numerose characteristicas instabile. Currentemente istes es:</p>
|
||||
<p xml:lang="it">A causa della natura dello sviluppo delle specifiche Matrix, NeoChat supporta anche numerose funzionalità instabili. Attualmente queste sono:</p>
|
||||
<p xml:lang="ka">Matrix-ის სპეციფიკაციის განვითარების ბუნების გამო NeoChat-ს ასევე აქვს უამრავი არასტაბილური ფუნქციაც. ახლა ისინია:</p>
|
||||
<p xml:lang="ko">Matrix 표준 개발의 특징으로 인하여 NeoChat은 일부 실험적인 기능을 지원합니다. 현재 지원하는 기능은 다음과 같습니다.</p>
|
||||
<p xml:lang="nl">Vanwege de aard van de ontwikkeling van de Matrix specificatie ondersteunt NeoChat ook talloze onstabiele mogelijkheden. Dit zijn nu:</p>
|
||||
<p xml:lang="nn">På grunn av måten Matrix-spesifikasjonen vert utvikla på, støttar NeoChat òg nokre uferdige funksjonar:</p>
|
||||
<p xml:lang="pl">Ze względu na sposób rozwoju Matriksa, NeoChat obsługuje także kilka niestabilnych możliwości. Obecnie są to:</p>
|
||||
<p xml:lang="pt">Devido à natureza do desenvolvimento da especificação do Matrix, o NeoChat também suporta diversas funcionalidades instáveis. De momento são:</p>
|
||||
<p xml:lang="sl">Zaradi narave razvoja specifikacije Matrixa NeoChat podpira tudi številne nestabilne zmožnosti. Trenutno so to:</p>
|
||||
<p xml:lang="sv">På grund av sättet Matrix-specifikationens utvecklas, stöder NeoChat också ett stor antal instabila funktioner. För närvarande är de:</p>
|
||||
<p xml:lang="ta">மேட்ரிக்ஸு நெறிமுறை வரையறுக்கப்படும் விதத்தின் காரணமாக, பல நிலையற்ற அம்சங்களையும் நியோச்சாட் ஆதரிக்கிறது. தற்போது ஆதரிக்கப்படுபவை:</p>
|
||||
<p xml:lang="tr">NeoChat, Matrix belirtimi geliştirmesinin doğası gereği çok sayıda kararsız özelliği de destekler. Şu anda bunlar:</p>
|
||||
<p xml:lang="uk">Через природу розробки специфікації Matrix, у NeoChat також передбачено підтримку численних нестабільних можливостей. У поточній версії цими можливостями є:</p>
|
||||
<p xml:lang="x-test">xxDue to the nature of the Matrix specification development NeoChat also supports numerous unstable features. Currently these are:xx</p>
|
||||
@@ -143,15 +151,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Enquestes - MSC3381</li>
|
||||
<li xml:lang="ca-valencia">Enquestes - MSC3381</li>
|
||||
<li xml:lang="en-GB">Polls - MSC3381</li>
|
||||
<li xml:lang="eo">Enketoj - MSC3381</li>
|
||||
<li xml:lang="es">Encuestas - MSC3381</li>
|
||||
<li xml:lang="eu">Polls - MSC3381</li>
|
||||
<li xml:lang="fi">Kyselyt – MSC3381</li>
|
||||
<li xml:lang="fr">Sondages - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas - MSC3381</li>
|
||||
<li xml:lang="gl">Enquisas — MSC3381</li>
|
||||
<li xml:lang="ia">Inquestas - MSC3381</li>
|
||||
<li xml:lang="it">Sondaggi - MSC3381</li>
|
||||
<li xml:lang="ka">Polls - MSC3381</li>
|
||||
<li xml:lang="ko">투표 - MSC3381</li>
|
||||
<li xml:lang="nl">Polls - MSC3381</li>
|
||||
<li xml:lang="nn">Avstemmingar – MSC3381</li>
|
||||
<li xml:lang="pl">Ankiety - MSC3381</li>
|
||||
<li xml:lang="pt">Inquéritos - MSC3381</li>
|
||||
<li xml:lang="sl">Polls - MSC3381</li>
|
||||
<li xml:lang="sv">Polls - MSC3381</li>
|
||||
<li xml:lang="ta">வாக்கெடுப்புகள் - MSC3381</li>
|
||||
<li xml:lang="tr">Anketler - MSC3381</li>
|
||||
<li xml:lang="uk">Опитування - MSC3381</li>
|
||||
@@ -161,15 +176,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="ca-valencia">Paquets d'adhesius - MSC2545</li>
|
||||
<li xml:lang="en-GB">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="eo">Glumark-Pakoj - MSC2545</li>
|
||||
<li xml:lang="es">Paquetes de pegatinas - MSC2545</li>
|
||||
<li xml:lang="eu">Eranskailu paketeak - MSC2545</li>
|
||||
<li xml:lang="fi">Tarrapakkaukset – MSC2545</li>
|
||||
<li xml:lang="fr">Paquets d'auto-collants - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos - MSC2545</li>
|
||||
<li xml:lang="gl">Paquetes de adhesivos — MSC2545</li>
|
||||
<li xml:lang="ia">Etiquetta gummate (sticker) -MSC2545</li>
|
||||
<li xml:lang="it">Pacchetti di adesivi - MSC2545</li>
|
||||
<li xml:lang="ka">სტიკერების პაკეტები - MSC2545</li>
|
||||
<li xml:lang="ko">스티커 팩 - MSC2545</li>
|
||||
<li xml:lang="nl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="nn">Klistremerke-pakkar – MSC2545</li>
|
||||
<li xml:lang="pl">Paczki naklejek - MSC2545</li>
|
||||
<li xml:lang="pt">Pacotes de Autocolantes - MSC2545</li>
|
||||
<li xml:lang="sl">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="sv">Sticker Packs - MSC2545</li>
|
||||
<li xml:lang="ta">ஒட்டி தொகுப்புகள் - MSC2545</li>
|
||||
<li xml:lang="tr">Yapışkan Paketleri - MSC2545</li>
|
||||
<li xml:lang="uk">Пакунки наліпок - MSC2545</li>
|
||||
@@ -179,15 +201,22 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<li xml:lang="ca">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="ca-valencia">Esdeveniments d'ubicació - MSC3488</li>
|
||||
<li xml:lang="en-GB">Location Events - MSC3488</li>
|
||||
<li xml:lang="eo">Lokaj Eventoj - MSC3488</li>
|
||||
<li xml:lang="es">Eventos de ubicación - MSC3488</li>
|
||||
<li xml:lang="eu">Kokaleku-gertaerak - MSC3488</li>
|
||||
<li xml:lang="fi">Sijaintitapahtumat – MSC3488</li>
|
||||
<li xml:lang="fr">Événements de lieu - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos - MSC3488</li>
|
||||
<li xml:lang="gl">Localización de eventos — MSC3488</li>
|
||||
<li xml:lang="ia">Eventos de Location - MSC3488</li>
|
||||
<li xml:lang="it">Località eventi - MSC3488</li>
|
||||
<li xml:lang="ka">მდებარეობის მოვლენები - MSC3488</li>
|
||||
<li xml:lang="ko">위치 이벤트 - MSC3488</li>
|
||||
<li xml:lang="nl">Locatie gebeurtenissen - MSC3488</li>
|
||||
<li xml:lang="nn">Posisjonshendingar – MSC3488</li>
|
||||
<li xml:lang="pl">Wydarzenia w miejscach - MSC3488</li>
|
||||
<li xml:lang="pt">Eventos com Localizações - MSC3488</li>
|
||||
<li xml:lang="sl">Location Events - MSC3488</li>
|
||||
<li xml:lang="sv">Location Events - MSC3488</li>
|
||||
<li xml:lang="ta">இட நிகழ்வுகள் - MSC3488</li>
|
||||
<li xml:lang="tr">Konum Etkinlikleri - MSC3488</li>
|
||||
<li xml:lang="uk">Місцеві зустрічі - MSC3488</li>
|
||||
@@ -208,6 +237,7 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<developer_name xml:lang="de">Die KDE-Gemeinschaft</developer_name>
|
||||
<developer_name xml:lang="el">Η Κοινότητα του KDE</developer_name>
|
||||
<developer_name xml:lang="en-GB">The KDE Community</developer_name>
|
||||
<developer_name xml:lang="eo">La KDE-Komunumo</developer_name>
|
||||
<developer_name xml:lang="es">La comunidad KDE</developer_name>
|
||||
<developer_name xml:lang="eu">KDE komunitatea</developer_name>
|
||||
<developer_name xml:lang="fi">KDE-yhteisö</developer_name>
|
||||
@@ -241,6 +271,10 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<value key="KDE::matrix">#neochat:kde.org</value>
|
||||
<value key="KDE::windows_store">https://www.microsoft.com/store/apps/9PNXWVNRC29H</value>
|
||||
<value key="KDE::mastodon">https://kde.social/@neochat</value>
|
||||
<value key="KDE::windows_store::StoreLogo9x16">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-720x1080.png</value>
|
||||
<value key="KDE::windows_store::StoreLogoSquare">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/storelogo-1080x1080.png</value>
|
||||
<value key="KDE::windows_store::Icon">https://invent.kde.org/network/neochat/-/raw/master/icons/300-apps-neochat.png</value>
|
||||
<value key="KDE::windows_store::PromotionalArt16x9">https://invent.kde.org/network/neochat/-/raw/master/icons/windows/promoimage-1920x1080.png</value>
|
||||
</custom>
|
||||
<launchable type="desktop-id">org.kde.neochat.desktop</launchable>
|
||||
<screenshots>
|
||||
@@ -250,14 +284,83 @@ to provide a convergent experience across multiple platforms.</p>
|
||||
<screenshot type="default">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/application.png</image>
|
||||
</screenshot>
|
||||
<screenshot environment="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Timeline.png</image>
|
||||
<caption>Main view with room list, chat, and room information</caption>
|
||||
<caption xml:lang="ar">العرض الرئيسة مع قائمة الغرف والدردشات و معلومات الغرفة</caption>
|
||||
<caption xml:lang="ca">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="ca-valencia">Vista principal amb la llista de sales, xats i informació de les sales</caption>
|
||||
<caption xml:lang="eo">Ĉefa vido kun ĉambra listo, babilejo kaj ĉambra informo</caption>
|
||||
<caption xml:lang="es">Vista principal con la lista de salas, chat e información de la sala</caption>
|
||||
<caption xml:lang="eu">Ikuspegi nagusia gela-zerrenda, berriketa, eta gelako informazioarekin</caption>
|
||||
<caption xml:lang="fi">Päänäkymä, jossa huoneluettelo, keskustelu ja huoneen tiedot</caption>
|
||||
<caption xml:lang="fr">Vue principale avec la liste des salons ainsi que des informations sur les salons et forums de discussions</caption>
|
||||
<caption xml:lang="gl">Vista principal coa lista de salas, a charla, e información da sala.</caption>
|
||||
<caption xml:lang="ia">Vista principal con lista de sala, chat e information de sala</caption>
|
||||
<caption xml:lang="it">Vista principale con elenco delle stanze, chat e informazioni sulla stanza</caption>
|
||||
<caption xml:lang="ka">მთავარი ხედი სურათების სიით, ჩატით და ოთახის ინფორმაციით</caption>
|
||||
<caption xml:lang="ko">대화방 목록, 채팅, 대화방 정보가 표시된 주 보기</caption>
|
||||
<caption xml:lang="nl">Hoofdweergave met lijst met rooms, chat en roominformatie</caption>
|
||||
<caption xml:lang="nn">Hovudvising med romliste, pratevindauge og rominformasjon</caption>
|
||||
<caption xml:lang="pl">Główny widok z wykazem pokojów, rozmowami i szczegółami pokojów</caption>
|
||||
<caption xml:lang="pt">A área principal com a lista de salas e com informações sobre a conversa e a sala</caption>
|
||||
<caption xml:lang="sl">Glavni pogled s seznamom sob, klepetom in informacijami o sobah</caption>
|
||||
<caption xml:lang="sv">Huvudvy med rumslista, chatt, och rumsinformation</caption>
|
||||
<caption xml:lang="ta">அரங்குப்பட்டியல், உரையாடல், மற்றும் அரங்குவிவரங்களைக் கொண்டுள்ள பிரதான காட்சி</caption>
|
||||
<caption xml:lang="tr">Oda listesini, sohbet penceresini ve oda bilgisini gösteren ana görünüm</caption>
|
||||
<caption xml:lang="uk">Головна панель із списком кімнат, спілкуванням та даними щодо кімнати</caption>
|
||||
<caption xml:lang="x-test">xxMain view with room list, chat, and room informationxx</caption>
|
||||
</screenshot>
|
||||
<screenshot environment="windows">
|
||||
<image>https://cdn.kde.org/screenshots/neochat/NeoChat-Windows-Login.png</image>
|
||||
<caption>Login screen</caption>
|
||||
<caption xml:lang="ar">شاشة الدخول</caption>
|
||||
<caption xml:lang="ca">Pantalla d'inici de sessió</caption>
|
||||
<caption xml:lang="ca-valencia">Pantalla d'inici de sessió</caption>
|
||||
<caption xml:lang="eo">Ensaluta ekrano</caption>
|
||||
<caption xml:lang="es">Pantalla de inicio de sesión</caption>
|
||||
<caption xml:lang="eu">Saio-hasteko pantaila</caption>
|
||||
<caption xml:lang="fi">Kirjautumisnäkymä</caption>
|
||||
<caption xml:lang="fr">Écran de connexion</caption>
|
||||
<caption xml:lang="gl">Pantalla de identificación.</caption>
|
||||
<caption xml:lang="ia">Schermo de accesso</caption>
|
||||
<caption xml:lang="it">Schermata di accesso</caption>
|
||||
<caption xml:lang="ka">შესვლის ეკრანი</caption>
|
||||
<caption xml:lang="ko">로그인 화면</caption>
|
||||
<caption xml:lang="nl">Aanmeldscherm</caption>
|
||||
<caption xml:lang="nn">Innloggingsbilete</caption>
|
||||
<caption xml:lang="pl">Ekran logowania</caption>
|
||||
<caption xml:lang="pt">Ecrã de autenticação</caption>
|
||||
<caption xml:lang="sl">Prijavni zaslon</caption>
|
||||
<caption xml:lang="sv">Inloggningsfönster</caption>
|
||||
<caption xml:lang="ta">நுழைவுத் திரை</caption>
|
||||
<caption xml:lang="tr">Oturum açma ekranı</caption>
|
||||
<caption xml:lang="uk">Вікно входу</caption>
|
||||
<caption xml:lang="x-test">xxLogin screenxx</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="23.08.3" date="2023-11-09"/>
|
||||
<release version="23.08.2" date="2023-10-12"/>
|
||||
<release version="23.08.0" date="2023-08-24">
|
||||
<url>https://kde.org/announcements/gear/23.08.0/#neochathttpsappskdeorgneochat</url>
|
||||
<description>
|
||||
<p>Apart from a visual overhaul, NeoChat can now display location events and also a map with the location of all the users currently broadcasting their location using Itineray's Matrix integration. Great for locating where your friends are.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="23.04.3" date="2023-07-06"/>
|
||||
<release version="23.04.2" date="2023-06-08"/>
|
||||
<release version="23.04.1" date="2023-05-11"/>
|
||||
<release version="23.04.0" date="2023-04-20">
|
||||
<url>https://kde.org/announcements/gear/23.04.0/#neochathttpsappskdeorgneochat</url>
|
||||
<description>
|
||||
<p>NeoChat improves its design with tweaks that provide a more compact layout and a simpler menu which works better for the collapsed room list.</p>
|
||||
<p>We have also improved the video controls, added a new command /knock <room-id> to send a knock event to a room, and you can now edit a prior message inline, within the chat pane.</p>
|
||||
<p>Other usability improvements include an overhaul of the keyboard navigation and shortcuts like Ctrl+PgUp/PgDn that allow you to skip from room to room.</p>
|
||||
</description>
|
||||
<artifacts>
|
||||
<artifact type="binary" platform="x86_64-windows-msvc">
|
||||
<location>https://download.kde.org/stable/release-service/23.04.0/windows/neochat-23.04.0-512-windows-cl-msvc2019-x86_64.exe</location>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
Version=1.5
|
||||
Name=NeoChat
|
||||
Name[ar]=نيوتشات
|
||||
Name[ast]=NeoChat
|
||||
Name[az]=NeoChat
|
||||
Name[ca]=NeoChat
|
||||
Name[ca@valencia]=NeoChat
|
||||
@@ -11,6 +12,7 @@ Name[cs]=NeoChat
|
||||
Name[de]=NeoChat
|
||||
Name[el]=NeoChat
|
||||
Name[en_GB]=NeoChat
|
||||
Name[eo]=NeoChat
|
||||
Name[es]=NeoChat
|
||||
Name[eu]=NeoChat
|
||||
Name[fi]=NeoChat
|
||||
@@ -49,6 +51,7 @@ GenericName[cs]=Klient protokolu Matrix
|
||||
GenericName[de]=Matrix-Programm
|
||||
GenericName[el]=Εφαρμογή του Matrix
|
||||
GenericName[en_GB]=Matrix Client
|
||||
GenericName[eo]=Matrix-Kliento
|
||||
GenericName[es]=Cliente para Matrix
|
||||
GenericName[eu]=Matrix bezeroa
|
||||
GenericName[fi]=Matrix-asiakas
|
||||
@@ -86,11 +89,12 @@ Comment[ca@valencia]=Client per al protocol Matrix
|
||||
Comment[de]=Programm für das Matrix-Protokoll
|
||||
Comment[el]=Πελάτης για το πρωτόκολλο Matrix
|
||||
Comment[en_GB]=Client for the Matrix protocol
|
||||
Comment[eo]=Kliento por la Matrix-protokolo
|
||||
Comment[es]=Cliente para el protocolo Matrix
|
||||
Comment[eu]=Matrix protokolorako bezeroa
|
||||
Comment[fi]=Asiakas Matrix-yhteyskäytännölle
|
||||
Comment[fr]=Client pour le protocole « Matrix »
|
||||
Comment[gl]=Cliente para o protocolo Matrix
|
||||
Comment[gl]=Cliente para o protocolo Matrix.
|
||||
Comment[hu]=Kliens a Matrix protokollhoz
|
||||
Comment[ia]=Cliente per le protocollo de Matrix
|
||||
Comment[id]=Klien untuk protokol Matrix
|
||||
@@ -100,7 +104,7 @@ Comment[ka]=კლიენტი Matrix-ის პროტოკოლის
|
||||
Comment[ko]=Matrix 프로토콜용 클라이언트
|
||||
Comment[lt]=Matrix protokolo kliento programa
|
||||
Comment[nl]=Client voor het Matrix-protocol
|
||||
Comment[nn]=Lynmeldingsklient for Matrix-protokollen
|
||||
Comment[nn]=Klient for Matrix-protokollen
|
||||
Comment[pa]=ਮੈਟਰਿਕਸ ਪਰੋਟੋਕਾਲ ਲਈ ਕਲਾਈਂਟ ਹੈ
|
||||
Comment[pl]=Program obsługi protokołu Matriksa
|
||||
Comment[pt]=Cliente para o protocolo Matrix
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<svg width="22" height="22" fill="none" version="1.1" id="svg13" xmlns="http://www.w3.org/2000/svg"><style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="M2 4h18v11H6.681L3 18.067V15H2zm1 10h1v1.933L6.319 14H19V5H3z" id="path3"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect5" d="M4 7h9v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect7" d="M4 9h7v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" id="rect9" d="M4 11h5v1H4z"/><path class="ColorScheme-Text" style="fill:currentColor;fill-opacity:1;stroke:none" fill-rule="evenodd" clip-rule="evenodd" d="m16 15.293-1.147-1.146-.707.707 2.853 2.853V14.5h-1z" id="path11"/></svg>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style type="text/css" id="current-color-scheme">.ColorScheme-Text{color:#232629}</style>
|
||||
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M3 3H19V14H8.68787L4 18.1019V14H3V3ZM4 13H5V15.8981L8.31213 13H18V4H4V13Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" fill-rule="evenodd" clip-rule="evenodd" d="M17 15.2929L14.8536 13.1465L14.1465 13.8536L18 17.7071V13.5H17V15.2929Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 6H15V7H5V6Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 8H13V9H5V8Z" fill="currentColor"/>
|
||||
<path class="ColorScheme-Text" d="M5 10H11V11H5V10Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 928 B After Width: | Height: | Size: 752 B |
6947
po/ar/neochat.po
6947
po/ar/neochat.po
File diff suppressed because it is too large
Load Diff
4499
po/ast/neochat.po
Normal file
4499
po/ast/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
6376
po/az/neochat.po
6376
po/az/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
>Vegeu també</title>
|
||||
<simplelist>
|
||||
<member
|
||||
>Una llista de les preguntes més freqüents quan a Matrix <ulink url="https://matrix.org/faq/"
|
||||
>Una llista de les preguntes més freqüents quant a Matrix <ulink url="https://matrix.org/faq/"
|
||||
>https://matrix.org/faq/</ulink
|
||||
> </member>
|
||||
<member
|
||||
|
||||
5595
po/ca/neochat.po
5595
po/ca/neochat.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6580
po/cs/neochat.po
6580
po/cs/neochat.po
File diff suppressed because it is too large
Load Diff
6433
po/da/neochat.po
6433
po/da/neochat.po
File diff suppressed because it is too large
Load Diff
5961
po/de/neochat.po
5961
po/de/neochat.po
File diff suppressed because it is too large
Load Diff
6639
po/el/neochat.po
6639
po/el/neochat.po
File diff suppressed because it is too large
Load Diff
6983
po/en_GB/neochat.po
6983
po/en_GB/neochat.po
File diff suppressed because it is too large
Load Diff
4555
po/eo/neochat.po
Normal file
4555
po/eo/neochat.po
Normal file
File diff suppressed because it is too large
Load Diff
5752
po/es/neochat.po
5752
po/es/neochat.po
File diff suppressed because it is too large
Load Diff
6793
po/eu/neochat.po
6793
po/eu/neochat.po
File diff suppressed because it is too large
Load Diff
6331
po/fi/neochat.po
6331
po/fi/neochat.po
File diff suppressed because it is too large
Load Diff
6213
po/fr/neochat.po
6213
po/fr/neochat.po
File diff suppressed because it is too large
Load Diff
5990
po/hu/neochat.po
5990
po/hu/neochat.po
File diff suppressed because it is too large
Load Diff
6683
po/ia/neochat.po
6683
po/ia/neochat.po
File diff suppressed because it is too large
Load Diff
5943
po/id/neochat.po
5943
po/id/neochat.po
File diff suppressed because it is too large
Load Diff
6724
po/ie/neochat.po
6724
po/ie/neochat.po
File diff suppressed because it is too large
Load Diff
5757
po/it/neochat.po
5757
po/it/neochat.po
File diff suppressed because it is too large
Load Diff
5908
po/ja/neochat.po
5908
po/ja/neochat.po
File diff suppressed because it is too large
Load Diff
5673
po/ka/neochat.po
5673
po/ka/neochat.po
File diff suppressed because it is too large
Load Diff
6888
po/ko/neochat.po
6888
po/ko/neochat.po
File diff suppressed because it is too large
Load Diff
5953
po/lt/neochat.po
5953
po/lt/neochat.po
File diff suppressed because it is too large
Load Diff
5743
po/nl/neochat.po
5743
po/nl/neochat.po
File diff suppressed because it is too large
Load Diff
6323
po/nn/neochat.po
6323
po/nn/neochat.po
File diff suppressed because it is too large
Load Diff
6988
po/pa/neochat.po
6988
po/pa/neochat.po
File diff suppressed because it is too large
Load Diff
5786
po/pl/neochat.po
5786
po/pl/neochat.po
File diff suppressed because it is too large
Load Diff
5913
po/pt/neochat.po
5913
po/pt/neochat.po
File diff suppressed because it is too large
Load Diff
6388
po/pt_BR/neochat.po
6388
po/pt_BR/neochat.po
File diff suppressed because it is too large
Load Diff
6526
po/ru/neochat.po
6526
po/ru/neochat.po
File diff suppressed because it is too large
Load Diff
6611
po/sk/neochat.po
6611
po/sk/neochat.po
File diff suppressed because it is too large
Load Diff
5857
po/sl/neochat.po
5857
po/sl/neochat.po
File diff suppressed because it is too large
Load Diff
122
po/sv/docs/neochat/man-neochat.1.docbook
Normal file
122
po/sv/docs/neochat/man-neochat.1.docbook
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE refentry PUBLIC "-//KDE//DTD DocBook XML V4.5-Based Variant V1.1//EN" "dtd/kdedbx45.dtd" [
|
||||
<!ENTITY % Swedish "INCLUDE">
|
||||
]>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
-->
|
||||
|
||||
<refentry lang="&language;">
|
||||
<refentryinfo>
|
||||
<title
|
||||
>NeoChat användarmanual</title>
|
||||
<author
|
||||
><firstname
|
||||
>Carl</firstname
|
||||
><surname
|
||||
>Schwan</surname
|
||||
> <contrib
|
||||
>NeoChat manualsida.</contrib
|
||||
> <email
|
||||
>carl@carlschwan.eu</email
|
||||
></author>
|
||||
<date
|
||||
>2022-11-01</date>
|
||||
<releaseinfo
|
||||
>22.09</releaseinfo>
|
||||
<productname
|
||||
>NeoChat</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>
|
||||
<command
|
||||
>neochat</command>
|
||||
</refentrytitle>
|
||||
<manvolnum
|
||||
>1</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname
|
||||
>neochat</refname>
|
||||
<refpurpose
|
||||
>Klient för att interagera med meddelandeprotokollet matrix</refpurpose>
|
||||
</refnamediv>
|
||||
<!-- body begins here -->
|
||||
<refsynopsisdiv id='synopsis'>
|
||||
<cmdsynopsis
|
||||
><command
|
||||
>neochat</command
|
||||
> <arg choice="opt"
|
||||
><replaceable
|
||||
>URI</replaceable
|
||||
></arg
|
||||
> </cmdsynopsis>
|
||||
</refsynopsisdiv>
|
||||
|
||||
|
||||
<refsect1 id="description">
|
||||
<title
|
||||
>Beskrivning</title>
|
||||
<para
|
||||
><command
|
||||
>neochat</command
|
||||
> är ett chattprogram för protokollet matrix som fungerar både på skrivbord och mobil. </para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="options"
|
||||
><title
|
||||
>Väljare</title>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term
|
||||
><option
|
||||
>URI</option
|
||||
></term>
|
||||
<listitem>
|
||||
<para
|
||||
>Matrix webbadress för en användare eller ett rum, t.ex. matrix:u/användare:exempel.org. Det gör att NeoChat försöker öppna det angivna rummet eller konversationen. </para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="bug">
|
||||
<title
|
||||
>Rapportera fel</title>
|
||||
<para
|
||||
>Rapportera fel och funktionsönskemål på <ulink url="https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General"
|
||||
>https://bugs.kde.org/enter_bug.cgi?product=NeoChat&component=General</ulink
|
||||
></para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title
|
||||
>Se också</title>
|
||||
<simplelist>
|
||||
<member
|
||||
>En lista över vanliga frågor om Matrix <ulink url="https://matrix.org/faq/"
|
||||
>https://matrix.org/faq/</ulink
|
||||
> </member>
|
||||
<member
|
||||
>kf5options(7)</member>
|
||||
<member
|
||||
>qt5options(7)</member>
|
||||
</simplelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1 id="copyright"
|
||||
><title
|
||||
>Copyright</title>
|
||||
<para
|
||||
>Copyright © 2020-2022 Tobias Fella </para>
|
||||
<para
|
||||
>Copyright © 2020-2022 Carl Schwan </para>
|
||||
<para
|
||||
>Licens: GNU General Public Version 3 eller senare <<ulink url="https://wwwgnuorg/licenses/gpl-html"
|
||||
>http://www.gnu.org/licenses/gpl-3.0.html</ulink
|
||||
>></para>
|
||||
</refsect1>
|
||||
</refentry>
|
||||
6797
po/sv/neochat.po
6797
po/sv/neochat.po
File diff suppressed because it is too large
Load Diff
5745
po/ta/neochat.po
5745
po/ta/neochat.po
File diff suppressed because it is too large
Load Diff
6802
po/tok/neochat.po
6802
po/tok/neochat.po
File diff suppressed because it is too large
Load Diff
5592
po/tr/neochat.po
5592
po/tr/neochat.po
File diff suppressed because it is too large
Load Diff
6326
po/uk/neochat.po
6326
po/uk/neochat.po
File diff suppressed because it is too large
Load Diff
6463
po/zh_CN/neochat.po
6463
po/zh_CN/neochat.po
File diff suppressed because it is too large
Load Diff
5908
po/zh_TW/neochat.po
5908
po/zh_TW/neochat.po
File diff suppressed because it is too large
Load Diff
@@ -5,132 +5,391 @@
|
||||
|
||||
add_library(neochat STATIC
|
||||
controller.cpp
|
||||
controller.h
|
||||
actionshandler.cpp
|
||||
actionshandler.h
|
||||
models/emojimodel.cpp
|
||||
models/emojimodel.h
|
||||
emojitones.cpp
|
||||
emojitones.h
|
||||
models/customemojimodel.cpp
|
||||
models/customemojimodel.h
|
||||
clipboard.cpp
|
||||
clipboard.h
|
||||
matriximageprovider.cpp
|
||||
matriximageprovider.h
|
||||
models/messageeventmodel.cpp
|
||||
models/messageeventmodel.h
|
||||
models/messagefiltermodel.cpp
|
||||
models/messagefiltermodel.h
|
||||
models/roomlistmodel.cpp
|
||||
models/roomlistmodel.h
|
||||
models/sortfilterspacelistmodel.cpp
|
||||
models/sortfilterspacelistmodel.h
|
||||
models/accountemoticonmodel.cpp
|
||||
models/accountemoticonmodel.h
|
||||
spacehierarchycache.cpp
|
||||
spacehierarchycache.h
|
||||
roommanager.cpp
|
||||
roommanager.h
|
||||
neochatroom.cpp
|
||||
neochatuser.cpp
|
||||
neochatroom.h
|
||||
models/userlistmodel.cpp
|
||||
models/userlistmodel.h
|
||||
models/userfiltermodel.cpp
|
||||
models/userfiltermodel.h
|
||||
models/publicroomlistmodel.cpp
|
||||
models/publicroomlistmodel.h
|
||||
models/spacechildrenmodel.cpp
|
||||
models/spacechildrenmodel.h
|
||||
models/spacechildsortfiltermodel.cpp
|
||||
models/spacechildsortfiltermodel.h
|
||||
models/spacetreeitem.cpp
|
||||
models/spacetreeitem.h
|
||||
models/userdirectorylistmodel.cpp
|
||||
models/keywordnotificationrulemodel.cpp
|
||||
models/userdirectorylistmodel.h
|
||||
models/pushrulemodel.cpp
|
||||
models/pushrulemodel.h
|
||||
models/emoticonfiltermodel.cpp
|
||||
models/emoticonfiltermodel.h
|
||||
notificationsmanager.cpp
|
||||
notificationsmanager.h
|
||||
models/sortfilterroomlistmodel.cpp
|
||||
models/sortfilterroomlistmodel.h
|
||||
chatdocumenthandler.cpp
|
||||
chatdocumenthandler.h
|
||||
models/devicesmodel.cpp
|
||||
filetypesingleton.cpp
|
||||
models/devicesmodel.h
|
||||
models/devicesproxymodel.cpp
|
||||
filetype.cpp
|
||||
filetype.h
|
||||
login.cpp
|
||||
login.h
|
||||
models/webshortcutmodel.cpp
|
||||
models/webshortcutmodel.h
|
||||
blurhash.cpp
|
||||
blurhash.h
|
||||
blurhashimageprovider.cpp
|
||||
models/collapsestateproxymodel.cpp
|
||||
blurhashimageprovider.h
|
||||
models/mediamessagefiltermodel.cpp
|
||||
models/mediamessagefiltermodel.h
|
||||
urlhelper.cpp
|
||||
urlhelper.h
|
||||
windowcontroller.cpp
|
||||
windowcontroller.h
|
||||
linkpreviewer.cpp
|
||||
linkpreviewer.h
|
||||
models/completionmodel.cpp
|
||||
models/completionmodel.h
|
||||
models/completionproxymodel.cpp
|
||||
models/completionproxymodel.h
|
||||
models/actionsmodel.cpp
|
||||
models/actionsmodel.h
|
||||
models/serverlistmodel.cpp
|
||||
models/serverlistmodel.h
|
||||
models/statemodel.cpp
|
||||
models/statemodel.h
|
||||
models/statefiltermodel.cpp
|
||||
models/statefiltermodel.h
|
||||
filetransferpseudojob.cpp
|
||||
filetransferpseudojob.h
|
||||
models/searchmodel.cpp
|
||||
models/searchmodel.h
|
||||
texthandler.cpp
|
||||
texthandler.h
|
||||
logger.cpp
|
||||
logger.h
|
||||
models/stickermodel.cpp
|
||||
models/stickermodel.h
|
||||
models/imagepacksmodel.cpp
|
||||
models/imagepacksmodel.h
|
||||
events/imagepackevent.cpp
|
||||
events/imagepackevent.h
|
||||
events/joinrulesevent.cpp
|
||||
events/stickerevent.cpp
|
||||
events/joinrulesevent.h
|
||||
models/reactionmodel.cpp
|
||||
models/reactionmodel.h
|
||||
delegatesizehelper.cpp
|
||||
delegatesizehelper.h
|
||||
models/livelocationsmodel.cpp
|
||||
models/livelocationsmodel.h
|
||||
models/locationsmodel.cpp
|
||||
models/locationsmodel.h
|
||||
locationhelper.cpp
|
||||
locationhelper.h
|
||||
events/pollevent.cpp
|
||||
pollhandler.cpp
|
||||
utils.h
|
||||
registration.cpp
|
||||
neochatconnection.cpp
|
||||
neochatconnection.h
|
||||
jobs/neochatdeactivateaccountjob.cpp
|
||||
jobs/neochatdeactivateaccountjob.h
|
||||
jobs/neochatdeletedevicejob.cpp
|
||||
jobs/neochatdeletedevicejob.h
|
||||
jobs/neochatchangepasswordjob.cpp
|
||||
jobs/neochatchangepasswordjob.h
|
||||
mediasizehelper.cpp
|
||||
mediasizehelper.h
|
||||
eventhandler.cpp
|
||||
enums/delegatetype.h
|
||||
roomlastmessageprovider.cpp
|
||||
roomlastmessageprovider.h
|
||||
chatbarcache.cpp
|
||||
chatbarcache.h
|
||||
colorschemer.cpp
|
||||
colorschemer.h
|
||||
models/notificationsmodel.cpp
|
||||
models/notificationsmodel.h
|
||||
models/timelinemodel.cpp
|
||||
models/timelinemodel.h
|
||||
)
|
||||
|
||||
qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN
|
||||
QML_FILES
|
||||
qml/main.qml
|
||||
qml/AccountMenu.qml
|
||||
qml/ExploreComponent.qml
|
||||
qml/ExploreComponentMobile.qml
|
||||
qml/ContextMenu.qml
|
||||
qml/CollapsedRoomDelegate.qml
|
||||
qml/RoomDelegate.qml
|
||||
qml/RoomListPage.qml
|
||||
qml/SpaceListContextMenu.qml
|
||||
qml/UserInfo.qml
|
||||
qml/UserInfoDesktop.qml
|
||||
qml/RoomPage.qml
|
||||
qml/RoomWindow.qml
|
||||
qml/JoinRoomPage.qml
|
||||
qml/ManualRoomDialog.qml
|
||||
qml/ExplorerDelegate.qml
|
||||
qml/InviteUserPage.qml
|
||||
qml/StartChatPage.qml
|
||||
qml/ImageEditorPage.qml
|
||||
qml/WelcomePage.qml
|
||||
qml/General.qml
|
||||
qml/RoomSecurity.qml
|
||||
qml/PushNotification.qml
|
||||
qml/Categories.qml
|
||||
qml/Permissions.qml
|
||||
qml/NeochatMaximizeComponent.qml
|
||||
qml/FancyEffectsContainer.qml
|
||||
qml/TypingPane.qml
|
||||
qml/QuickSwitcher.qml
|
||||
qml/HoverActions.qml
|
||||
qml/ChatBar.qml
|
||||
qml/AttachmentPane.qml
|
||||
qml/ReplyPane.qml
|
||||
qml/CompletionMenu.qml
|
||||
qml/PieProgressBar.qml
|
||||
qml/QuickFormatBar.qml
|
||||
qml/RoomData.qml
|
||||
qml/ServerData.qml
|
||||
qml/EmojiPicker.qml
|
||||
qml/TimelineDelegate.qml
|
||||
qml/ReplyComponent.qml
|
||||
qml/StateDelegate.qml
|
||||
qml/RichLabel.qml
|
||||
qml/MessageDelegate.qml
|
||||
qml/Bubble.qml
|
||||
qml/SectionDelegate.qml
|
||||
qml/VideoDelegate.qml
|
||||
qml/ReactionDelegate.qml
|
||||
qml/LinkPreviewDelegate.qml
|
||||
qml/AudioDelegate.qml
|
||||
qml/FileDelegate.qml
|
||||
qml/ImageDelegate.qml
|
||||
qml/EncryptedDelegate.qml
|
||||
qml/EventDelegate.qml
|
||||
qml/TextDelegate.qml
|
||||
qml/ReadMarkerDelegate.qml
|
||||
qml/PollDelegate.qml
|
||||
qml/MimeComponent.qml
|
||||
qml/StateComponent.qml
|
||||
qml/MessageEditComponent.qml
|
||||
qml/AvatarFlow.qml
|
||||
qml/LoginStep.qml
|
||||
qml/Login.qml
|
||||
qml/Homeserver.qml
|
||||
qml/Username.qml
|
||||
qml/RegisterPassword.qml
|
||||
qml/Captcha.qml
|
||||
qml/Terms.qml
|
||||
qml/Email.qml
|
||||
qml/Password.qml
|
||||
qml/LoginRegister.qml
|
||||
qml/Loading.qml
|
||||
qml/LoginMethod.qml
|
||||
qml/Sso.qml
|
||||
qml/UserDetailDialog.qml
|
||||
qml/CreateRoomDialog.qml
|
||||
qml/EmojiDialog.qml
|
||||
qml/OpenFileDialog.qml
|
||||
qml/KeyVerificationDialog.qml
|
||||
qml/ConfirmLogoutDialog.qml
|
||||
qml/PowerLevelDialog.qml
|
||||
qml/Message.qml
|
||||
qml/EmojiItem.qml
|
||||
qml/EmojiRow.qml
|
||||
qml/EmojiSas.qml
|
||||
qml/ConfirmDeactivateAccountDialog.qml
|
||||
qml/VerificationCanceled.qml
|
||||
qml/GlobalMenu.qml
|
||||
qml/EditMenu.qml
|
||||
qml/MessageDelegateContextMenu.qml
|
||||
qml/FileDelegateContextMenu.qml
|
||||
qml/MessageSourceSheet.qml
|
||||
qml/ReportSheet.qml
|
||||
qml/SettingsPage.qml
|
||||
qml/ThemeRadioButton.qml
|
||||
qml/ColorScheme.qml
|
||||
qml/GeneralSettingsPage.qml
|
||||
qml/EmoticonsPage.qml
|
||||
qml/EmoticonEditorPage.qml
|
||||
qml/EmoticonFormCard.qml
|
||||
qml/GlobalNotificationsPage.qml
|
||||
qml/NotificationRuleItem.qml
|
||||
qml/AppearanceSettingsPage.qml
|
||||
qml/AccountsPage.qml
|
||||
qml/AccountEditorPage.qml
|
||||
qml/DevicesPage.qml
|
||||
qml/DeviceDelegate.qml
|
||||
qml/DevicesCard.qml
|
||||
qml/About.qml
|
||||
qml/AboutKDE.qml
|
||||
qml/SonnetConfigPage.qml
|
||||
qml/NetworkProxyPage.qml
|
||||
qml/DevtoolsPage.qml
|
||||
qml/ConfirmEncryptionDialog.qml
|
||||
qml/RemoveSheet.qml
|
||||
qml/BanSheet.qml
|
||||
qml/EmojiTonesPicker.qml
|
||||
qml/EmojiDelegate.qml
|
||||
qml/EmojiGrid.qml
|
||||
qml/SearchPage.qml
|
||||
qml/LocationDelegate.qml
|
||||
qml/LocationChooser.qml
|
||||
qml/TimelineView.qml
|
||||
qml/InvitationView.qml
|
||||
qml/AvatarTabButton.qml
|
||||
qml/SpaceDrawer.qml
|
||||
qml/OsmLocationPlugin.qml
|
||||
qml/LiveLocationDelegate.qml
|
||||
qml/FullScreenMap.qml
|
||||
qml/LocationsPage.qml
|
||||
qml/LocationMapItem.qml
|
||||
qml/RoomDrawer.qml
|
||||
qml/RoomDrawerPage.qml
|
||||
qml/DirectChatDrawerHeader.qml
|
||||
qml/GroupChatDrawerHeader.qml
|
||||
qml/RoomInformation.qml
|
||||
qml/RoomMedia.qml
|
||||
qml/ChooseRoomDialog.qml
|
||||
qml/ShareAction.qml
|
||||
qml/SpaceHomePage.qml
|
||||
qml/SpaceHierarchyDelegate.qml
|
||||
qml/RemoveChildDialog.qml
|
||||
qml/SelectParentDialog.qml
|
||||
qml/Security.qml
|
||||
qml/QrCodeMaximizeComponent.qml
|
||||
qml/SelectSpacesDialog.qml
|
||||
qml/AttachDialog.qml
|
||||
qml/NotificationsView.qml
|
||||
qml/LoadingDelegate.qml
|
||||
qml/TimelineEndDelegate.qml
|
||||
RESOURCES
|
||||
qml/confetti.png
|
||||
qml/glowdot.png
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(neochat PROPERTIES OUTPUT_NAME "neochatlib")
|
||||
endif()
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "messageeventmodel_logging.h"
|
||||
IDENTIFIER "MessageEvent"
|
||||
CATEGORY_NAME "org.kde.neochat.messageeventmodel"
|
||||
DESCRIPTION "Neochat: messageeventmodel"
|
||||
DEFAULT_SEVERITY Info
|
||||
EXPORT NEOCHAT
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "eventhandler_logging.h"
|
||||
IDENTIFIER "EventHandling"
|
||||
CATEGORY_NAME "org.kde.neochat.eventhandler"
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(neochat
|
||||
HEADER "voip_logging.h"
|
||||
IDENTIFIER "voip"
|
||||
CATEGORY_NAME "org.kde.neochat.voip"
|
||||
HEADER "chatdocumenthandler_logging.h"
|
||||
IDENTIFIER "ChatDocumentHandling"
|
||||
CATEGORY_NAME "org.kde.neochat.chatdocumenthandler"
|
||||
DEFAULT_SEVERITY Info
|
||||
)
|
||||
|
||||
add_executable(neochat-app
|
||||
main.cpp
|
||||
res.qrc
|
||||
)
|
||||
|
||||
if(TARGET Qt::WebView)
|
||||
target_link_libraries(neochat-app PUBLIC Qt::WebView)
|
||||
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
|
||||
|
||||
target_link_libraries(neochat-app PRIVATE
|
||||
neochat
|
||||
)
|
||||
|
||||
if(Quotient${QUOTIENT_SUFFIX}_VERSION_MINOR GREATER 6)
|
||||
target_compile_definitions(neochat PUBLIC QUOTIENT_07)
|
||||
target_sources(neochat PRIVATE events/pollevent.cpp pollhandler.cpp)
|
||||
else()
|
||||
target_compile_definitions(neochat PUBLIC QUOTIENT_VERSION=\"${Quotient${QUOTIENT_SUFFIX}_VERSION}\")
|
||||
target_sources(neochat PRIVATE neochataccountregistry.cpp)
|
||||
endif()
|
||||
|
||||
ecm_add_app_icon(NEOCHAT_ICON ICONS ${CMAKE_SOURCE_DIR}/128-logo.png)
|
||||
|
||||
target_sources(neochat-app PRIVATE ${NEOCHAT_ICON})
|
||||
|
||||
if(NOT ANDROID)
|
||||
target_sources(neochat PRIVATE colorschemer.cpp)
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat PRIVATE trayicon_sni.cpp)
|
||||
target_sources(neochat PRIVATE trayicon_sni.cpp trayicon_sni.h)
|
||||
target_link_libraries(neochat PRIVATE KF6::StatusNotifierItem)
|
||||
else()
|
||||
target_sources(neochat PRIVATE trayicon.cpp)
|
||||
target_sources(neochat PRIVATE trayicon.cpp trayicon.h)
|
||||
endif()
|
||||
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::ConfigWidgets KF${QT_MAJOR_VERSION}::WindowSystem)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_COLORSCHEME)
|
||||
target_link_libraries(neochat PUBLIC KF6::WindowSystem ICU::uc)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_WINDOWSYSTEM)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_ICU)
|
||||
endif()
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
target_sources(neochat-app PRIVATE res_desktop.qrc)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_RUNNER)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_X11)
|
||||
target_sources(neochat PRIVATE runner.cpp)
|
||||
else()
|
||||
target_sources(neochat-app PRIVATE res_android.qrc)
|
||||
endif()
|
||||
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark ${QTKEYCHAIN_LIBRARIES} QCoro::Core)
|
||||
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_CURRENT_SOURCE_DIR}/enums)
|
||||
target_link_libraries(neochat PUBLIC
|
||||
Qt::Core
|
||||
Qt::Quick
|
||||
Qt::Qml
|
||||
Qt::Gui
|
||||
Qt::Multimedia
|
||||
Qt::Network
|
||||
Qt::QuickControls2
|
||||
KF6::I18n
|
||||
KF6::Kirigami
|
||||
KF6::Notifications
|
||||
KF6::ConfigCore
|
||||
KF6::ConfigGui
|
||||
KF6::CoreAddons
|
||||
KF6::SonnetCore
|
||||
KF6::ColorScheme
|
||||
KF6::ItemModels
|
||||
QuotientQt6
|
||||
cmark::cmark
|
||||
QCoro::Core
|
||||
QCoro::Network
|
||||
)
|
||||
|
||||
if (TARGET PkgConfig::GSTREAMER)
|
||||
target_link_libraries(neochat PUBLIC PkgConfig::GSTREAMER)
|
||||
target_sources(neochat PRIVATE
|
||||
call/callmanager.cpp
|
||||
call/callsession.cpp
|
||||
call/audiosources.cpp
|
||||
call/videosources.cpp
|
||||
call/devicemonitor.cpp
|
||||
models/callparticipantsmodel.cpp
|
||||
call/callparticipant.cpp
|
||||
)
|
||||
target_compile_definitions(neochat PUBLIC GSTREAMER_AVAILABLE)
|
||||
endif()
|
||||
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||
|
||||
if(NEOCHAT_FLATPAK)
|
||||
@@ -215,9 +474,11 @@ if(ANDROID)
|
||||
"preferences-desktop-notification"
|
||||
"computer-symbolic"
|
||||
"gps"
|
||||
"system-users-symbolic"
|
||||
)
|
||||
ecm_add_android_apk(neochat-app ANDROID_DIR ${CMAKE_SOURCE_DIR}/android)
|
||||
else()
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||
target_link_libraries(neochat PUBLIC Qt::Widgets KF6::KIOWidgets)
|
||||
install(FILES neochat.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR})
|
||||
endif()
|
||||
|
||||
@@ -225,18 +486,27 @@ if(NOT ANDROID)
|
||||
set_target_properties(neochat-app PROPERTIES OUTPUT_NAME "neochat")
|
||||
endif()
|
||||
|
||||
if(TARGET KF${QT_MAJOR_VERSION}::DBusAddons)
|
||||
target_link_libraries(neochat PUBLIC KF${QT_MAJOR_VERSION}::DBusAddons)
|
||||
if(TARGET KF6::DBusAddons)
|
||||
target_link_libraries(neochat PUBLIC KF6::DBusAddons)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KDBUSADDONS)
|
||||
endif()
|
||||
|
||||
if (TARGET KF${QT_MAJOR_VERSION}::KIOWidgets)
|
||||
if (TARGET KF6::KIOWidgets)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
if (TARGET KUnifiedPush)
|
||||
target_compile_definitions(neochat PUBLIC -DHAVE_KUNIFIEDPUSH)
|
||||
target_link_libraries(neochat PUBLIC KUnifiedPush)
|
||||
|
||||
if (NOT ANDROID)
|
||||
configure_file(org.kde.neochat.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.neochat.service DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
install(TARGETS neochat-app ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
|
||||
if (NOT ANDROID AND NOT WIN32 AND NOT APPLE)
|
||||
install(FILES plasma-runner-neochat.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
#include "actionshandler.h"
|
||||
|
||||
#include <csapi/joining.h>
|
||||
#include <events/roommemberevent.h>
|
||||
#include <Quotient/csapi/joining.h>
|
||||
#include <Quotient/events/roommemberevent.h>
|
||||
|
||||
#include <cmark.h>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include "models/actionsmodel.h"
|
||||
#include "models/customemojimodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "neochatroom.h"
|
||||
#include "texthandler.h"
|
||||
|
||||
using namespace Quotient;
|
||||
@@ -39,45 +38,33 @@ void ActionsHandler::setRoom(NeoChatRoom *room)
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
void ActionsHandler::handleNewMessage()
|
||||
void ActionsHandler::handleMessageEvent(ChatBarCache *chatBarCache)
|
||||
{
|
||||
checkEffects(m_room->chatBoxText());
|
||||
if (!m_room->chatBoxAttachmentPath().isEmpty()) {
|
||||
QUrl url(m_room->chatBoxAttachmentPath());
|
||||
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||
m_room->uploadFile(path, m_room->chatBoxText().isEmpty() ? path.mid(path.lastIndexOf('/') + 1) : m_room->chatBoxText());
|
||||
m_room->setChatBoxAttachmentPath({});
|
||||
m_room->setChatBoxText({});
|
||||
if (!chatBarCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString handledText = m_room->chatBoxText();
|
||||
handledText = handleMentions(handledText);
|
||||
handleMessage(m_room->chatBoxText(), handledText);
|
||||
checkEffects(chatBarCache->text());
|
||||
if (!chatBarCache->attachmentPath().isEmpty()) {
|
||||
QUrl url(chatBarCache->attachmentPath());
|
||||
auto path = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||
m_room->uploadFile(QUrl(path), chatBarCache->text().isEmpty() ? path.mid(path.lastIndexOf(u'/') + 1) : chatBarCache->text());
|
||||
chatBarCache->setAttachmentPath({});
|
||||
chatBarCache->setText({});
|
||||
return;
|
||||
}
|
||||
|
||||
QString handledText = chatBarCache->text();
|
||||
handledText = handleMentions(handledText, chatBarCache->mentions());
|
||||
handleMessage(m_room->mainCache()->text(), handledText, chatBarCache);
|
||||
}
|
||||
|
||||
void ActionsHandler::handleEdit()
|
||||
{
|
||||
checkEffects(m_room->editText());
|
||||
|
||||
QString handledText = m_room->editText();
|
||||
handledText = handleMentions(handledText, true);
|
||||
handleMessage(m_room->editText(), handledText, true);
|
||||
}
|
||||
|
||||
QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
|
||||
QString ActionsHandler::handleMentions(QString handledText, QList<Mention> *mentions)
|
||||
{
|
||||
if (!m_room) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QVector<Mention> *mentions;
|
||||
if (isEdit) {
|
||||
mentions = m_room->editMentions();
|
||||
} else {
|
||||
mentions = m_room->mentions();
|
||||
}
|
||||
|
||||
std::sort(mentions->begin(), mentions->end(), [](const auto &a, const auto &b) -> bool {
|
||||
return a.cursor.anchor() > b.cursor.anchor();
|
||||
});
|
||||
@@ -88,17 +75,17 @@ QString ActionsHandler::handleMentions(QString handledText, const bool &isEdit)
|
||||
}
|
||||
handledText = handledText.replace(mention.cursor.anchor(),
|
||||
mention.cursor.position() - mention.cursor.anchor(),
|
||||
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text, mention.id));
|
||||
QStringLiteral("[%1](https://matrix.to/#/%2)").arg(mention.text.toHtmlEscaped(), mention.id));
|
||||
}
|
||||
mentions->clear();
|
||||
|
||||
return handledText;
|
||||
}
|
||||
|
||||
void ActionsHandler::handleMessage(const QString &text, QString handledText, const bool &isEdit)
|
||||
void ActionsHandler::handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache)
|
||||
{
|
||||
if (NeoChatConfig::allowQuickEdit()) {
|
||||
QRegularExpression sed("^s/([^/]*)/([^/]*)(/g)?$");
|
||||
QRegularExpression sed(QStringLiteral("^s/([^/]*)/([^/]*)(/g)?$"));
|
||||
auto match = sed.match(text);
|
||||
if (match.hasMatch()) {
|
||||
const QString regex = match.captured(1);
|
||||
@@ -114,13 +101,13 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
} else {
|
||||
originalString = event->plainBody();
|
||||
}
|
||||
if (flags == "/g") {
|
||||
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), "", event->id());
|
||||
if (flags == "/g"_ls) {
|
||||
m_room->postHtmlMessage(handledText, originalString.replace(regex, replacement), event->msgtype(), {}, event->id());
|
||||
} else {
|
||||
m_room->postHtmlMessage(handledText,
|
||||
originalString.replace(originalString.indexOf(regex), regex.size(), replacement),
|
||||
event->msgtype(),
|
||||
"",
|
||||
{},
|
||||
event->id());
|
||||
}
|
||||
return;
|
||||
@@ -134,8 +121,8 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
if (handledText.startsWith(QLatin1Char('/'))) {
|
||||
for (const auto &action : ActionsModel::instance().allActions()) {
|
||||
if (handledText.indexOf(action.prefix) == 1
|
||||
&& (handledText.indexOf(" ") == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
|
||||
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room);
|
||||
&& (handledText.indexOf(" "_ls) == action.prefix.length() + 1 || handledText.length() == action.prefix.length() + 1)) {
|
||||
handledText = action.handle(handledText.mid(action.prefix.length() + 1).trimmed(), m_room, chatBarCache);
|
||||
if (action.messageType.has_value()) {
|
||||
messageType = *action.messageType;
|
||||
}
|
||||
@@ -153,33 +140,35 @@ void ActionsHandler::handleMessage(const QString &text, QString handledText, con
|
||||
textHandler.setData(handledText);
|
||||
handledText = textHandler.handleSendText();
|
||||
|
||||
if (handledText.count("<p>") == 1 && handledText.count("</p>") == 1) {
|
||||
handledText.remove("<p>");
|
||||
handledText.remove("</p>");
|
||||
if (handledText.count("<p>"_ls) == 1 && handledText.count("</p>"_ls) == 1) {
|
||||
handledText.remove("<p>"_ls);
|
||||
handledText.remove("</p>"_ls);
|
||||
}
|
||||
|
||||
if (handledText.length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_room->postMessage(text, handledText, messageType, m_room->chatBoxReplyId(), isEdit ? m_room->chatBoxEditId() : "");
|
||||
m_room->postMessage(text, handledText, messageType, chatBarCache->replyId(), chatBarCache->editId(), chatBarCache->threadId());
|
||||
}
|
||||
|
||||
void ActionsHandler::checkEffects(const QString &text)
|
||||
{
|
||||
std::optional<QString> effect = std::nullopt;
|
||||
if (text.contains("\u2744")) {
|
||||
if (text.contains(QStringLiteral("\u2744"))) {
|
||||
effect = QLatin1String("snowflake");
|
||||
} else if (text.contains("\u1F386")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F386"))) {
|
||||
effect = QLatin1String("fireworks");
|
||||
} else if (text.contains("\u2F387")) {
|
||||
} else if (text.contains(QStringLiteral("\u2F387"))) {
|
||||
effect = QLatin1String("fireworks");
|
||||
} else if (text.contains("\u1F389")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F389"))) {
|
||||
effect = QLatin1String("confetti");
|
||||
} else if (text.contains("\u1F38A")) {
|
||||
} else if (text.contains(QStringLiteral("\u1F38A"))) {
|
||||
effect = QLatin1String("confetti");
|
||||
}
|
||||
if (effect.has_value()) {
|
||||
Q_EMIT showEffect(*effect);
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_actionshandler.cpp"
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <events/roommessageevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class CustomEmojiModel;
|
||||
class NeoChatRoom;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,7 @@ class NeoChatRoom;
|
||||
class ActionsHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The room that messages will be sent to.
|
||||
@@ -47,24 +49,18 @@ public:
|
||||
|
||||
Q_SIGNALS:
|
||||
void roomChanged();
|
||||
void showEffect(QString effect);
|
||||
void showEffect(const QString &effect);
|
||||
|
||||
public Q_SLOTS:
|
||||
|
||||
/**
|
||||
* @brief Pre-process text and send message.
|
||||
* @brief Pre-process text and send message event.
|
||||
*/
|
||||
void handleNewMessage();
|
||||
|
||||
/**
|
||||
* @brief Pre-process text and send edit.
|
||||
*/
|
||||
void handleEdit();
|
||||
void handleMessageEvent(ChatBarCache *chatBarCache);
|
||||
|
||||
private:
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
void checkEffects(const QString &text);
|
||||
|
||||
QString handleMentions(QString handledText, const bool &isEdit = false);
|
||||
void handleMessage(const QString &text, QString handledText, const bool &isEdit = false);
|
||||
QString handleMentions(QString handledText, QList<Mention> *mentions);
|
||||
void handleMessage(const QString &text, QString handledText, ChatBarCache *chatBarCache);
|
||||
};
|
||||
|
||||
@@ -177,7 +177,7 @@ uint8_t *decode(const char *blurhash, int width, int height, int punch, int nCha
|
||||
uint8_t *pixelArray = createByteArray(bytesPerRow * height);
|
||||
|
||||
if (decodeToArray(blurhash, width, height, punch, nChannels, pixelArray) == -1) {
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
return pixelArray;
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "audiosources.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
int AudioSources::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return DeviceMonitor::instance().audioSources().size();
|
||||
}
|
||||
|
||||
QVariant AudioSources::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() >= DeviceMonitor::instance().audioSources().size()) {
|
||||
return QVariant(QStringLiteral("DEADBEEF"));
|
||||
}
|
||||
if (role == TitleRole) {
|
||||
return DeviceMonitor::instance().audioSources()[index.row()]->title;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AudioSources::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TitleRole, "title"},
|
||||
};
|
||||
}
|
||||
|
||||
AudioSources::AudioSources()
|
||||
: QAbstractListModel()
|
||||
{
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::audioSourceAdded, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::audioSourceRemoved, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
}
|
||||
|
||||
GstDevice *AudioSources::currentDevice() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->microphone();
|
||||
for (const auto &audioSource : DeviceMonitor::instance().audioSources()) {
|
||||
if (audioSource->title == name) {
|
||||
qDebug() << "WebRTC: microphone:" << name;
|
||||
return audioSource->device;
|
||||
}
|
||||
}
|
||||
return DeviceMonitor::instance().audioSources()[0]->device;
|
||||
}
|
||||
|
||||
void AudioSources::setCurrentIndex(int index) const
|
||||
{
|
||||
if (DeviceMonitor::instance().audioSources().size() == 0) {
|
||||
return;
|
||||
}
|
||||
NeoChatConfig::setMicrophone(DeviceMonitor::instance().audioSources()[index]->title);
|
||||
NeoChatConfig::self()->save();
|
||||
}
|
||||
|
||||
int AudioSources::currentIndex() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->microphone();
|
||||
if (name.isEmpty()) {
|
||||
return getDefaultDeviceIndex();
|
||||
}
|
||||
for (auto i = 0; i < DeviceMonitor::instance().audioSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().audioSources()[i]->title == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int AudioSources::getDefaultDeviceIndex() const
|
||||
{
|
||||
for (auto i = 0; i < DeviceMonitor::instance().audioSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().audioSources()[i]->isDefault) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
class AudioSources : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
TitleRole = Qt::UserRole + 1,
|
||||
};
|
||||
|
||||
static AudioSources &instance()
|
||||
{
|
||||
static AudioSources _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
GstDevice *currentDevice() const;
|
||||
|
||||
void setCurrentIndex(int index) const;
|
||||
int currentIndex() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentIndexChanged();
|
||||
|
||||
private:
|
||||
AudioSources();
|
||||
int getDefaultDeviceIndex() const;
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "calldevices.h"
|
||||
#include "audiodevicesmodel.h"
|
||||
#include "neochatconfig.h"
|
||||
#include "videodevicesmodel.h"
|
||||
#include <QStringView>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
|
||||
#include "voiplogging.h"
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
extern "C" {
|
||||
#include "gst/gst.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef GSTREAMER_AVAILABLE
|
||||
|
||||
CallDevices::CallDevices()
|
||||
: QObject()
|
||||
, m_audioDevicesModel(new AudioDevicesModel(this))
|
||||
, m_videoDevicesModel(new VideoDevicesModel(this))
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
AudioDevicesModel *CallDevices::audioDevicesModel() const
|
||||
{
|
||||
return m_audioDevicesModel;
|
||||
}
|
||||
|
||||
VideoDevicesModel *CallDevices::videoDevicesModel() const
|
||||
{
|
||||
return m_videoDevicesModel;
|
||||
}
|
||||
|
||||
void CallDevices::addDevice(GstDevice *device)
|
||||
{
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
gchar *type = gst_device_get_device_class(device);
|
||||
bool isVideo = !std::strncmp(type, "Video", 5);
|
||||
g_free(type);
|
||||
if (isVideo) {
|
||||
m_videoDevicesModel->addDevice(device);
|
||||
m_videoDevicesModel->setDefaultDevice();
|
||||
} else {
|
||||
m_audioDevicesModel->addDevice(device);
|
||||
m_audioDevicesModel->setDefaultDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void CallDevices::removeDevice(GstDevice *device, bool changed)
|
||||
{
|
||||
if (device) {
|
||||
if (m_audioDevicesModel->removeDevice(device, changed) || m_videoDevicesModel->removeDevice(device, changed))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
gboolean newBusMessage(GstBus *bus, GstMessage *msg, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus)
|
||||
Q_UNUSED(user_data)
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_added(msg, &device);
|
||||
CallDevices::instance().addDevice(device);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
GstDevice *device;
|
||||
gst_message_parse_device_removed(msg, &device);
|
||||
CallDevices::instance().removeDevice(device, false);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_CHANGED: {
|
||||
GstDevice *device;
|
||||
GstDevice *oldDevice;
|
||||
gst_message_parse_device_changed(msg, &device, &oldDevice);
|
||||
CallDevices::instance().removeDevice(oldDevice, true);
|
||||
CallDevices::instance().addDevice(device);
|
||||
Q_EMIT CallDevices::instance().devicesChanged();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void CallDevices::init()
|
||||
{
|
||||
static GstDeviceMonitor *monitor = nullptr;
|
||||
if (!monitor) {
|
||||
monitor = gst_device_monitor_new();
|
||||
Q_ASSERT(monitor);
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Audio/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
caps = gst_caps_new_empty_simple("video/x-raw");
|
||||
gst_device_monitor_add_filter(monitor, "Video/Source", caps);
|
||||
gst_device_monitor_add_filter(monitor, "Video/Duplex", caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBus *bus = gst_device_monitor_get_bus(monitor);
|
||||
gst_bus_add_watch(bus, newBusMessage, nullptr);
|
||||
gst_object_unref(bus);
|
||||
if (!gst_device_monitor_start(monitor)) {
|
||||
qCCritical(voip) << "Failed to start device monitor";
|
||||
return;
|
||||
} else {
|
||||
qCDebug(voip) << "Device monitor started";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CallDevices::hasMicrophone() const
|
||||
{
|
||||
return m_audioDevicesModel->hasMicrophone();
|
||||
}
|
||||
|
||||
bool CallDevices::hasCamera() const
|
||||
{
|
||||
return m_videoDevicesModel->hasCamera();
|
||||
}
|
||||
|
||||
QStringList CallDevices::resolutions(const QString &cameraName) const
|
||||
{
|
||||
return m_videoDevicesModel->resolutions(cameraName);
|
||||
}
|
||||
|
||||
QStringList CallDevices::frameRates(const QString &cameraName, const QString &resolution) const
|
||||
{
|
||||
if (auto s = m_videoDevicesModel->getVideoSource(cameraName); s) {
|
||||
if (auto it = std::find_if(s->caps.cbegin(),
|
||||
s->caps.cend(),
|
||||
[&](const auto &c) {
|
||||
return c.resolution == resolution;
|
||||
});
|
||||
it != s->caps.cend())
|
||||
return it->frameRates;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GstDevice *CallDevices::audioDevice() const
|
||||
{
|
||||
return m_audioDevicesModel->currentDevice();
|
||||
}
|
||||
|
||||
GstDevice *CallDevices::videoDevice(QPair<int, int> &resolution, QPair<int, int> &frameRate) const
|
||||
{
|
||||
return m_videoDevicesModel->currentDevice(resolution, frameRate);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool CallDevices::hasMicrophone() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CallDevices::hasCamera() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList CallDevices::names(bool, const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList CallDevices::resolutions(const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
QStringList CallDevices::frameRates(const QString &, const QString &) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,64 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
typedef struct _GstDevice GstDevice;
|
||||
|
||||
class CallDevices;
|
||||
class AudioDevicesModel;
|
||||
class VideoDevicesModel;
|
||||
|
||||
class CallDevices : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(AudioDevicesModel *audioDevices READ audioDevicesModel CONSTANT);
|
||||
Q_PROPERTY(VideoDevicesModel *videoDevices READ videoDevicesModel CONSTANT);
|
||||
|
||||
public:
|
||||
static CallDevices &instance()
|
||||
{
|
||||
static CallDevices instance;
|
||||
return instance;
|
||||
}
|
||||
CallDevices(CallDevices const &) = delete;
|
||||
void operator=(CallDevices const &) = delete;
|
||||
|
||||
bool hasMicrophone() const;
|
||||
bool hasCamera() const;
|
||||
QStringList names(bool isVideo, const QString &defaultDevice) const;
|
||||
QStringList resolutions(const QString &cameraName) const;
|
||||
QStringList frameRates(const QString &cameraName, const QString &resolution) const;
|
||||
|
||||
AudioDevicesModel *audioDevicesModel() const;
|
||||
VideoDevicesModel *videoDevicesModel() const;
|
||||
|
||||
void addDevice(GstDevice *device);
|
||||
void removeDevice(GstDevice *device, bool changed);
|
||||
|
||||
Q_SIGNALS:
|
||||
void devicesChanged();
|
||||
|
||||
private:
|
||||
CallDevices();
|
||||
|
||||
void init();
|
||||
GstDevice *audioDevice() const;
|
||||
GstDevice *videoDevice(QPair<int, int> &resolution, QPair<int, int> &frameRate) const;
|
||||
|
||||
AudioDevicesModel *m_audioDevicesModel;
|
||||
VideoDevicesModel *m_videoDevicesModel;
|
||||
|
||||
friend class CallSession;
|
||||
friend class Audio;
|
||||
};
|
||||
@@ -1,617 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020-2021 Nheko Authors
|
||||
// SPDX-FileCopyrightText: 2021-2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "callmanager.h"
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "voiplogging.h"
|
||||
#include <KLocalizedString>
|
||||
#include <QDateTime>
|
||||
|
||||
#include <QMediaPlaylist>
|
||||
#include <QMimeDatabase>
|
||||
#include <qcoro/qcorosignal.h>
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#include "neochatconfig.h"
|
||||
|
||||
#define CALL_VERSION "1"
|
||||
|
||||
CallManager::CallManager()
|
||||
{
|
||||
init();
|
||||
connect(&Controller::instance(), &Controller::activeConnectionChanged, this, [this] {
|
||||
updateTurnServers();
|
||||
});
|
||||
}
|
||||
|
||||
QCoro::Task<void> CallManager::updateTurnServers()
|
||||
{
|
||||
if (m_cachedTurnUrisValidUntil > QDateTime::currentDateTime()) {
|
||||
co_return;
|
||||
}
|
||||
Controller::instance().activeConnection()->getTurnServers();
|
||||
|
||||
auto servers = co_await qCoro(Controller::instance().activeConnection(), &Connection::turnServersChanged);
|
||||
m_cachedTurnUrisValidUntil = QDateTime::currentDateTime().addSecs(servers["ttl"].toInt());
|
||||
|
||||
const auto password = servers["password"].toString();
|
||||
const auto username = servers["username"].toString();
|
||||
const auto uris = servers["uris"].toArray();
|
||||
|
||||
m_cachedTurnUris.clear();
|
||||
for (const auto &u : uris) {
|
||||
QString uri = u.toString();
|
||||
auto c = uri.indexOf(':');
|
||||
if (c == -1) {
|
||||
qCWarning(voip) << "Invalid TURN URI:" << uri;
|
||||
continue;
|
||||
}
|
||||
QString scheme = uri.left(c);
|
||||
if (scheme != "turn" && scheme != "turns") {
|
||||
qCWarning(voip) << "Invalid TURN scheme:" << scheme;
|
||||
continue;
|
||||
}
|
||||
m_cachedTurnUris += QStringLiteral("%1://%2:%3@%4").arg(scheme, QUrl::toPercentEncoding(username), QUrl::toPercentEncoding(password), uri.mid(c + 1));
|
||||
}
|
||||
}
|
||||
|
||||
QString CallManager::callId() const
|
||||
{
|
||||
return m_callId;
|
||||
}
|
||||
|
||||
void CallManager::handleCallEvent(NeoChatRoom *room, const Quotient::RoomEvent *event)
|
||||
{
|
||||
if (const auto &inviteEvent = eventCast<const CallInviteEvent>(event)) {
|
||||
handleInvite(room, inviteEvent);
|
||||
} else if (const auto &hangupEvent = eventCast<const CallHangupEvent>(event)) {
|
||||
handleHangup(room, hangupEvent);
|
||||
} else if (const auto &candidatesEvent = eventCast<const CallCandidatesEvent>(event)) {
|
||||
handleCandidates(room, candidatesEvent);
|
||||
} else if (const auto &answerEvent = eventCast<const CallAnswerEvent>(event)) {
|
||||
handleAnswer(room, answerEvent);
|
||||
} else if (const auto &negotiateEvent = eventCast<const CallNegotiateEvent>(event)) {
|
||||
handleNegotiate(room, negotiateEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void CallManager::checkStartCall()
|
||||
{
|
||||
if ((m_incomingCandidates.isEmpty() && !m_incomingSdp.contains("candidates"_ls)) || m_incomingSdp.isEmpty()) {
|
||||
qCDebug(voip) << "Not ready to start this call yet";
|
||||
return;
|
||||
}
|
||||
m_session->acceptAnswer(m_incomingSdp, m_incomingCandidates, m_remoteUser->id());
|
||||
m_incomingCandidates.clear();
|
||||
m_incomingSdp.clear();
|
||||
setGlobalState(ACTIVE);
|
||||
}
|
||||
|
||||
void CallManager::handleAnswer(NeoChatRoom *room, const Quotient::CallAnswerEvent *event)
|
||||
{
|
||||
if (globalState() != OUTGOING) {
|
||||
qCDebug(voip) << "Ignoring answer while in state" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Ignoring answer for unknown call id" << event->callId() << ". Our call id is" << m_callId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id() && partyId() == event->contentJson()["party_id"].toString()) {
|
||||
qCDebug(voip) << "Ignoring echo for answer";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Call was accepted on a different device";
|
||||
// Show the user that call was accepted on a different device
|
||||
// Stop ringing
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO handle that MSC wrt to accepting on other devices
|
||||
m_session->setMetadata(event->contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject());
|
||||
m_remotePartyId = event->contentJson()["party_id"].toString();
|
||||
m_incomingSdp = event->sdp();
|
||||
checkStartCall();
|
||||
}
|
||||
|
||||
void CallManager::handleCandidates(NeoChatRoom *room, const Quotient::CallCandidatesEvent *event)
|
||||
{
|
||||
// TODO what if candidates come before invite? this looks wrong
|
||||
if (globalState() == IDLE) {
|
||||
qCDebug(voip) << "Ignoring candidates in state" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Ignoring candidates sent by ourself";
|
||||
return;
|
||||
}
|
||||
|
||||
if (globalState() == ACTIVE) {
|
||||
QVector<Candidate> candidates;
|
||||
for (const auto &candidate : event->candidates()) {
|
||||
candidates += Candidate{candidate.toObject()["candidate"].toString(),
|
||||
candidate.toObject()["sdpMLineIndex"].toInt(),
|
||||
candidate.toObject()["sdpMid"].toString()};
|
||||
}
|
||||
m_session->acceptCandidates(candidates);
|
||||
return;
|
||||
}
|
||||
|
||||
qCDebug(voip) << "Storing" << event->candidates().size() << "incoming candidates";
|
||||
for (const auto &candidate : event->candidates()) {
|
||||
m_incomingCandidates +=
|
||||
Candidate{candidate.toObject()["candidate"].toString(), candidate.toObject()["sdpMLineIndex"].toInt(), candidate.toObject()["sdpMid"].toString()};
|
||||
}
|
||||
|
||||
if (globalState() == OUTGOING) {
|
||||
checkStartCall();
|
||||
}
|
||||
}
|
||||
|
||||
void CallManager::handleInvite(NeoChatRoom *room, const Quotient::CallInviteEvent *event)
|
||||
{
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Igoring invite sent by ourself";
|
||||
return;
|
||||
}
|
||||
if (globalState() != IDLE) {
|
||||
// TODO handle glare
|
||||
qCDebug(voip) << "Ignoring invite while already in a call";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->originTimestamp() < QDateTime::currentDateTime().addSecs(-60)) {
|
||||
qCDebug(voip) << "Ignoring outdated invite; sent at:" << event->originTimestamp() << "current:" << QDateTime::currentDateTime();
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobalState(INCOMING);
|
||||
|
||||
m_incomingSdp = event->sdp();
|
||||
setRemoteUser(dynamic_cast<NeoChatUser *>(room->user(event->senderId())));
|
||||
setRoom(room);
|
||||
setCallId(event->callId());
|
||||
setPartyId(generatePartyId());
|
||||
m_remotePartyId = event->contentJson()["party_id"].toString();
|
||||
setLifetime(event->lifetime());
|
||||
Q_EMIT incomingCall(remoteUser(), room, event->lifetime(), callId());
|
||||
ring(event->lifetime());
|
||||
}
|
||||
|
||||
void CallManager::handleNegotiate(NeoChatRoom *room, const Quotient::CallNegotiateEvent *event)
|
||||
{
|
||||
Q_UNUSED(room);
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown call id" << event->callId() << ". Our call id is" << m_callId;
|
||||
return;
|
||||
}
|
||||
if (event->partyId() != m_remotePartyId) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown party id" << event->partyId() << ". Remote party id is" << m_remotePartyId;
|
||||
return;
|
||||
}
|
||||
if (event->senderId() != m_remoteUser->id()) {
|
||||
qCDebug(voip) << "Ignoring negotiate for unknown user id" << event->senderId() << ". Remote user id is" << m_remoteUser->id();
|
||||
return;
|
||||
}
|
||||
// TODO DUPLICATES FFS
|
||||
m_session->setMetadata(event->contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject());
|
||||
m_session->renegotiateOffer(event->sdp(), m_remoteUser->id(), event->contentJson()["description"]["type"] == QStringLiteral("answer"));
|
||||
}
|
||||
|
||||
void CallManager::ring(int lifetime)
|
||||
{
|
||||
// TODO put a better default ringtone in the kcfg
|
||||
// TODO which one? ship one? plasma-mobile-sounds?
|
||||
if (!QFileInfo::exists(NeoChatConfig::ringtone())) {
|
||||
qCWarning(voip) << "Ringtone file doesn't exist. Not audibly ringing";
|
||||
return;
|
||||
}
|
||||
auto ringtone = QUrl::fromLocalFile(NeoChatConfig::ringtone());
|
||||
m_playlist.setPlaybackMode(QMediaPlaylist::Loop);
|
||||
m_playlist.clear();
|
||||
m_ringPlayer.setPlaylist(&m_playlist);
|
||||
m_playlist.addMedia(ringtone);
|
||||
m_ringPlayer.play();
|
||||
QTimer::singleShot(lifetime, this, [this]() {
|
||||
stopRinging();
|
||||
Q_EMIT callEnded();
|
||||
});
|
||||
}
|
||||
|
||||
void CallManager::stopRinging()
|
||||
{
|
||||
m_ringPlayer.stop();
|
||||
}
|
||||
|
||||
void CallManager::handleHangup(NeoChatRoom *room, const Quotient::CallHangupEvent *event)
|
||||
{
|
||||
if (globalState() == IDLE) {
|
||||
qCDebug(voip) << "Ignoring hangup since we're not in a call";
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->senderId() == room->localUser()->id()) {
|
||||
qCDebug(voip) << "Ignoring hangup we sent ourselves";
|
||||
// TODO hangup-to-decline by different device?
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->callId() != m_callId) {
|
||||
qCDebug(voip) << "Hangup not for this call. Event's call id:" << event->callId() << ". Our call id" << m_callId;
|
||||
return;
|
||||
}
|
||||
|
||||
stopRinging();
|
||||
if (m_session) {
|
||||
m_session->end();
|
||||
delete m_session;
|
||||
}
|
||||
setGlobalState(IDLE);
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
|
||||
void CallManager::acceptCall()
|
||||
{
|
||||
// TODO metadata for this case
|
||||
if (globalState() != INCOMING) {
|
||||
qCWarning(voip) << "Not accepting call while state is" << globalState();
|
||||
return;
|
||||
}
|
||||
|
||||
stopRinging();
|
||||
|
||||
if (!checkPlugins()) {
|
||||
qCCritical(voip) << "Missing plugins; can't accept call";
|
||||
}
|
||||
|
||||
updateTurnServers();
|
||||
// TODO wait until candidates are here
|
||||
|
||||
m_session = CallSession::acceptCall(m_incomingSdp, m_incomingCandidates, m_cachedTurnUris, m_remoteUser->id(), this);
|
||||
m_participants->clear();
|
||||
connect(m_session.data(), &CallSession::stateChanged, this, [this] {
|
||||
Q_EMIT stateChanged();
|
||||
if (state() == CallSession::ICEFAILED) {
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
}); // TODO refactor away?
|
||||
m_incomingCandidates.clear();
|
||||
connectSingleShot(m_session.data(), &CallSession::answerCreated, this, [this](const QString &_sdp, const QVector<Candidate> &candidates) {
|
||||
const auto &[uuids, sdp] = mangleSdp(_sdp);
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
auto answer = createAnswer(m_callId, sdp, msidToPurpose);
|
||||
m_room->postJson("m.call.answer", answer);
|
||||
qCWarning(voip) << "Sending Answer";
|
||||
auto c = createCandidates(m_callId, candidates);
|
||||
auto cand = createCandidates(m_callId, candidates);
|
||||
m_room->postJson("m.call.candidates", cand);
|
||||
qCWarning(voip) << "Sending Candidates";
|
||||
setGlobalState(ACTIVE);
|
||||
});
|
||||
}
|
||||
|
||||
void CallManager::hangupCall()
|
||||
{
|
||||
qCDebug(voip) << "Ending call";
|
||||
if (m_session) {
|
||||
m_session->end();
|
||||
delete m_session;
|
||||
m_session = nullptr;
|
||||
}
|
||||
stopRinging();
|
||||
m_room->postJson("m.call.hangup", createHangup(m_callId));
|
||||
setGlobalState(IDLE);
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
|
||||
NeoChatUser *CallManager::remoteUser() const
|
||||
{
|
||||
return m_remoteUser;
|
||||
}
|
||||
|
||||
NeoChatRoom *CallManager::room() const
|
||||
{
|
||||
return m_room;
|
||||
}
|
||||
|
||||
CallSession::State CallManager::state() const
|
||||
{
|
||||
if (!m_session) {
|
||||
return CallSession::DISCONNECTED;
|
||||
}
|
||||
return m_session->state();
|
||||
}
|
||||
|
||||
int CallManager::lifetime() const
|
||||
{
|
||||
return m_lifetime;
|
||||
}
|
||||
|
||||
void CallManager::ignoreCall()
|
||||
{
|
||||
setLifetime(0);
|
||||
setCallId({});
|
||||
setRoom(nullptr);
|
||||
setRemoteUser(nullptr);
|
||||
}
|
||||
|
||||
void CallManager::startCall(NeoChatRoom *room)
|
||||
{
|
||||
if (m_session) {
|
||||
// Don't start calls if there already is one
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("A call is already started"));
|
||||
return;
|
||||
}
|
||||
if (room->users().size() != 2) {
|
||||
// Don't start calls if the room doesn't have exactly two members
|
||||
Q_EMIT Controller::instance().errorOccured(i18n("Calls are limited to 1:1 rooms"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto missingPlugins = CallSession::missingPlugins();
|
||||
if (!missingPlugins.isEmpty()) {
|
||||
qCCritical(voip) << "Missing GStreamer plugins:" << missingPlugins;
|
||||
Q_EMIT Controller::instance().errorOccured("Missing GStreamer plugins.");
|
||||
return;
|
||||
}
|
||||
|
||||
setLifetime(60000);
|
||||
setRoom(room);
|
||||
setRemoteUser(otherUser(room));
|
||||
|
||||
updateTurnServers();
|
||||
|
||||
setCallId(generateCallId());
|
||||
setPartyId(generatePartyId());
|
||||
|
||||
m_participants->clear();
|
||||
for (const auto &user : m_room->users()) {
|
||||
auto participant = new CallParticipant(m_session);
|
||||
participant->m_user = dynamic_cast<NeoChatUser *>(user);
|
||||
m_participants->addParticipant(participant);
|
||||
}
|
||||
|
||||
m_session = CallSession::startCall(m_cachedTurnUris, this);
|
||||
setGlobalState(OUTGOING);
|
||||
connect(m_session, &CallSession::stateChanged, this, [this] {
|
||||
Q_EMIT stateChanged();
|
||||
if (state() == CallSession::ICEFAILED) {
|
||||
Q_EMIT callEnded();
|
||||
}
|
||||
});
|
||||
|
||||
connectSingleShot(m_session.data(), &CallSession::offerCreated, this, [this](const QString &_sdp, const QVector<Candidate> &candidates) {
|
||||
const auto &[uuids, sdp] = mangleSdp(_sdp);
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
qCWarning(voip) << "Sending Invite";
|
||||
qCWarning(voip) << "Sending Candidates";
|
||||
auto invite = createInvite(m_callId, sdp, msidToPurpose);
|
||||
auto c = createCandidates(m_callId, candidates);
|
||||
m_room->postJson("m.call.invite", invite);
|
||||
m_room->postJson("m.call.candidates", c);
|
||||
});
|
||||
|
||||
connect(m_session, &CallSession::renegotiate, this, [this](const QString &sdp, const QString &type) {
|
||||
QVector<std::pair<QString, QString>> msidToPurpose;
|
||||
const auto &[uuids, _sdp] = mangleSdp(sdp);
|
||||
for (const auto &uuid : uuids) {
|
||||
msidToPurpose += {uuid, "m.usermedia"}; // TODO
|
||||
}
|
||||
QJsonObject json{
|
||||
{QStringLiteral("lifetime"), 60000},
|
||||
{QStringLiteral("version"), 1},
|
||||
{QStringLiteral("description"), QJsonObject{{QStringLiteral("type"), type}, {QStringLiteral("sdp"), _sdp}}}, // AAAAA
|
||||
{QStringLiteral("party_id"), m_partyId},
|
||||
{QStringLiteral("call_id"), m_callId},
|
||||
};
|
||||
QJsonObject metadata;
|
||||
for (const auto &[stream, purpose] : msidToPurpose) {
|
||||
QJsonObject data = {{"purpose", purpose}};
|
||||
metadata[stream] = data;
|
||||
}
|
||||
json["org.matrix.msc3077.sdp_stream_metadata"] = metadata;
|
||||
m_room->postJson("m.call.negotiate", json);
|
||||
});
|
||||
}
|
||||
|
||||
QString CallManager::generateCallId() const
|
||||
{
|
||||
return QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz");
|
||||
}
|
||||
|
||||
QString CallManager::generatePartyId() const
|
||||
{
|
||||
return QUuid::createUuid().toString();
|
||||
}
|
||||
|
||||
void CallManager::setCallId(const QString &callId)
|
||||
{
|
||||
m_callId = callId;
|
||||
Q_EMIT callIdChanged();
|
||||
}
|
||||
|
||||
void CallManager::setPartyId(const QString &partyId)
|
||||
{
|
||||
m_partyId = partyId;
|
||||
}
|
||||
|
||||
void CallManager::setMuted(bool muted)
|
||||
{
|
||||
if (!m_session) {
|
||||
return;
|
||||
}
|
||||
m_session->setMuted(muted);
|
||||
Q_EMIT mutedChanged();
|
||||
}
|
||||
|
||||
bool CallManager::muted() const
|
||||
{
|
||||
if (!m_session) {
|
||||
return false;
|
||||
}
|
||||
return m_session->muted();
|
||||
}
|
||||
|
||||
bool CallManager::init()
|
||||
{
|
||||
qRegisterMetaType<Candidate>();
|
||||
qRegisterMetaType<QVector<Candidate>>();
|
||||
GError *error = nullptr;
|
||||
if (!gst_init_check(nullptr, nullptr, &error)) {
|
||||
QString strError;
|
||||
if (error) {
|
||||
strError += error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
qCCritical(voip) << "Failed to initialize GStreamer:" << strError;
|
||||
return false;
|
||||
}
|
||||
|
||||
gchar *version = gst_version_string();
|
||||
qCDebug(voip) << "GStreamer version" << version;
|
||||
g_free(version);
|
||||
|
||||
// Required to register the qml types
|
||||
auto _sink = gst_element_factory_make("qmlglsink", nullptr);
|
||||
Q_ASSERT(_sink);
|
||||
gst_object_unref(_sink);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CallManager::setLifetime(int lifetime)
|
||||
{
|
||||
m_lifetime = lifetime;
|
||||
Q_EMIT lifetimeChanged();
|
||||
}
|
||||
|
||||
void CallManager::setRoom(NeoChatRoom *room)
|
||||
{
|
||||
m_room = room;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
void CallManager::setRemoteUser(NeoChatUser *user)
|
||||
{
|
||||
m_remoteUser = user;
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
NeoChatUser *CallManager::otherUser(NeoChatRoom *room)
|
||||
{
|
||||
return dynamic_cast<NeoChatUser *>(room->users()[0]->id() == room->localUser()->id() ? room->users()[1] : room->users()[0]);
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createCandidates(const QString &callId, const QVector<Candidate> &candidates) const
|
||||
{
|
||||
QJsonArray candidatesJson;
|
||||
for (const auto &candidate : candidates) {
|
||||
candidatesJson += QJsonObject{{"candidate", candidate.candidate}, {"sdpMid", candidate.sdpMid}, {"sdpMLineIndex", candidate.sdpMLineIndex}};
|
||||
}
|
||||
return QJsonObject{{"call_id", callId}, {"candidates", candidatesJson}, {"version", CALL_VERSION}, {"party_id", "todopartyid"}};
|
||||
}
|
||||
|
||||
void CallManager::setGlobalState(GlobalState globalState)
|
||||
{
|
||||
if (m_globalState == globalState) {
|
||||
return;
|
||||
}
|
||||
m_globalState = globalState;
|
||||
Q_EMIT globalStateChanged();
|
||||
}
|
||||
|
||||
CallManager::GlobalState CallManager::globalState() const
|
||||
{
|
||||
return m_globalState;
|
||||
}
|
||||
|
||||
CallParticipantsModel *CallManager::callParticipants() const
|
||||
{
|
||||
return m_participants;
|
||||
}
|
||||
|
||||
std::pair<QStringList, QString> CallManager::mangleSdp(const QString &_sdp)
|
||||
{
|
||||
QString sdp = _sdp;
|
||||
QRegularExpression regex("msid:user[0-9]+@host-[0-9a-f]+ webrtctransceiver([0-9])");
|
||||
auto iter = regex.globalMatch(sdp);
|
||||
QStringList uuids;
|
||||
|
||||
while (iter.hasNext()) {
|
||||
auto uuid = QUuid::createUuid();
|
||||
auto match = iter.next();
|
||||
uuids += uuid.toString();
|
||||
sdp.replace(match.captured(), QStringLiteral("msid:") + uuid.toString() + QStringLiteral(" foo"));
|
||||
}
|
||||
return {uuids, sdp};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createInvite(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const
|
||||
{
|
||||
QJsonObject metadata;
|
||||
for (const auto &[msid, purpose] : msidToPurpose) {
|
||||
metadata[msid] = QJsonObject{{"purpose", purpose}};
|
||||
}
|
||||
return {{"call_id", callId},
|
||||
{"party_id", "todopartyid"},
|
||||
{"lifetime", 60000},
|
||||
{"capabilities", QJsonObject{{"m.call.transferee", false}}},
|
||||
{"offer", QJsonObject{{"sdp", sdp}, {"type", "offer"}}},
|
||||
{"org.matrix.msc3077.sdp_stream_metadata", metadata},
|
||||
{"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createHangup(const QString &callId) const
|
||||
{
|
||||
return {{"call_id", callId}, {"party_id", "todopartyid"}, {"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
QJsonObject CallManager::createAnswer(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const
|
||||
{
|
||||
Q_ASSERT(!callId.isEmpty());
|
||||
QJsonObject metadata;
|
||||
for (const auto &[msid, purpose] : msidToPurpose) {
|
||||
metadata[msid] = QJsonObject{{"purpose", purpose}};
|
||||
}
|
||||
return {{"call_id", callId},
|
||||
{"party_id", "todopartyid"},
|
||||
{"lifetime", "lifetime"},
|
||||
{"capabilities", QJsonObject{{"m.call.transferee", false}}},
|
||||
{"offer", QJsonObject{{"sdp", sdp}, {"type", "offer"}}},
|
||||
{"org.matrix.msc3077.sdp_stream_metadata", metadata},
|
||||
{"version", CALL_VERSION}};
|
||||
}
|
||||
|
||||
void CallManager::toggleCamera()
|
||||
{
|
||||
m_session->toggleCamera();
|
||||
}
|
||||
QString CallManager::partyId() const
|
||||
{
|
||||
return m_partyId;
|
||||
}
|
||||
|
||||
bool CallManager::checkPlugins() const
|
||||
{
|
||||
auto missingPlugins = m_session->missingPlugins();
|
||||
if (!missingPlugins.isEmpty()) {
|
||||
qCCritical(voip) << "Missing GStreamer plugins:" << missingPlugins;
|
||||
Q_EMIT Controller::instance().errorOccured("Missing GStreamer plugins.");
|
||||
}
|
||||
return !missingPlugins.isEmpty();
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2020-2021 Nheko Authors
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "neochatuser.h"
|
||||
#include <QAbstractListModel>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <events/roomevent.h>
|
||||
|
||||
#include "callsession.h"
|
||||
|
||||
#include "models/callparticipantsmodel.h"
|
||||
#include <events/callevents.h>
|
||||
|
||||
#include <QMediaPlayer>
|
||||
#include <QMediaPlaylist>
|
||||
#include <QTimer>
|
||||
#include <qcoro/task.h>
|
||||
|
||||
#include <qobjectdefs.h>
|
||||
|
||||
class CallSession;
|
||||
class QQuickItem;
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
class CallManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum GlobalState {
|
||||
IDLE,
|
||||
INCOMING,
|
||||
OUTGOING,
|
||||
ACTIVE,
|
||||
};
|
||||
Q_ENUM(GlobalState);
|
||||
|
||||
Q_PROPERTY(GlobalState globalState READ globalState NOTIFY globalStateChanged)
|
||||
Q_PROPERTY(NeoChatUser *remoteUser READ remoteUser NOTIFY remoteUserChanged)
|
||||
Q_PROPERTY(QString callId READ callId NOTIFY callIdChanged)
|
||||
Q_PROPERTY(NeoChatRoom *room READ room NOTIFY roomChanged)
|
||||
Q_PROPERTY(int lifetime READ lifetime NOTIFY lifetimeChanged)
|
||||
Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)
|
||||
Q_PROPERTY(QQuickItem *item MEMBER m_item) // TODO allow for different devices for each session
|
||||
Q_PROPERTY(CallSession::State state READ state NOTIFY stateChanged)
|
||||
Q_PROPERTY(CallParticipantsModel *callParticipants READ callParticipants CONSTANT)
|
||||
|
||||
static CallManager &instance()
|
||||
{
|
||||
static CallManager _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString callId() const;
|
||||
[[nodiscard]] QString partyId() const;
|
||||
|
||||
CallSession::State state() const;
|
||||
|
||||
NeoChatUser *remoteUser() const;
|
||||
NeoChatRoom *room() const;
|
||||
|
||||
int lifetime() const;
|
||||
|
||||
bool muted() const;
|
||||
void setMuted(bool muted);
|
||||
|
||||
CallManager::GlobalState globalState() const;
|
||||
|
||||
void handleCallEvent(NeoChatRoom *room, const RoomEvent *event);
|
||||
|
||||
Q_INVOKABLE void startCall(NeoChatRoom *room);
|
||||
Q_INVOKABLE void acceptCall();
|
||||
Q_INVOKABLE void hangupCall();
|
||||
Q_INVOKABLE void ignoreCall();
|
||||
|
||||
Q_INVOKABLE void toggleCamera();
|
||||
|
||||
QCoro::Task<void> updateTurnServers();
|
||||
|
||||
[[nodiscard]] CallParticipantsModel *callParticipants() const;
|
||||
|
||||
QQuickItem *m_item = nullptr;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentCallIdChanged();
|
||||
void incomingCall(NeoChatUser *user, NeoChatRoom *room, int timeout, const QString &callId);
|
||||
void callEnded();
|
||||
void remoteUserChanged();
|
||||
void callIdChanged();
|
||||
void roomChanged();
|
||||
void stateChanged();
|
||||
void lifetimeChanged();
|
||||
void mutedChanged();
|
||||
void globalStateChanged();
|
||||
|
||||
private:
|
||||
CallManager();
|
||||
QString m_callId;
|
||||
|
||||
QVector<Candidate> m_incomingCandidates;
|
||||
QString m_incomingSdp;
|
||||
|
||||
[[nodiscard]] bool checkPlugins() const;
|
||||
|
||||
QStringList m_cachedTurnUris;
|
||||
QDateTime m_cachedTurnUrisValidUntil = QDateTime::fromSecsSinceEpoch(0);
|
||||
|
||||
NeoChatUser *m_remoteUser = nullptr;
|
||||
NeoChatRoom *m_room = nullptr;
|
||||
QString m_remotePartyId;
|
||||
QString m_partyId;
|
||||
int m_lifetime = 0;
|
||||
|
||||
GlobalState m_globalState = IDLE;
|
||||
|
||||
void handleInvite(NeoChatRoom *room, const CallInviteEvent *event);
|
||||
void handleHangup(NeoChatRoom *room, const CallHangupEvent *event);
|
||||
void handleCandidates(NeoChatRoom *room, const CallCandidatesEvent *event);
|
||||
void handleAnswer(NeoChatRoom *room, const CallAnswerEvent *event);
|
||||
void handleNegotiate(NeoChatRoom *room, const CallNegotiateEvent *event);
|
||||
void checkStartCall();
|
||||
|
||||
void ring(int lifetime);
|
||||
void stopRinging();
|
||||
|
||||
[[nodiscard]] QString generateCallId() const;
|
||||
[[nodiscard]] QString generatePartyId() const;
|
||||
bool init();
|
||||
|
||||
bool m_initialised = false;
|
||||
QPointer<CallSession> m_session = nullptr;
|
||||
|
||||
void setLifetime(int lifetime);
|
||||
void setRoom(NeoChatRoom *room);
|
||||
void setRemoteUser(NeoChatUser *user);
|
||||
void setCallId(const QString &callId);
|
||||
void setPartyId(const QString &partyId);
|
||||
void setGlobalState(GlobalState state);
|
||||
|
||||
std::pair<QStringList, QString> mangleSdp(const QString &sdp);
|
||||
|
||||
CallParticipantsModel *m_participants = new CallParticipantsModel();
|
||||
|
||||
NeoChatUser *otherUser(NeoChatRoom *room);
|
||||
|
||||
[[nodiscard]] QJsonObject createCandidates(const QString &callId, const QVector<Candidate> &candidates) const;
|
||||
[[nodiscard]] QJsonObject createInvite(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const;
|
||||
[[nodiscard]] QJsonObject createHangup(const QString &callId) const;
|
||||
[[nodiscard]] QJsonObject createAnswer(const QString &callId, const QString &sdp, const QVector<std::pair<QString, QString>> &msidToPurpose) const;
|
||||
|
||||
QMediaPlayer m_ringPlayer;
|
||||
QMediaPlaylist m_playlist;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "callnegotiateevent.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
CallNegotiateEvent::CallNegotiateEvent(const QString &callId,
|
||||
const QString &partyId,
|
||||
int lifetime,
|
||||
const QString &sdp,
|
||||
bool answer,
|
||||
QVector<std::pair<QString, QString>> msidToPurpose)
|
||||
: EventTemplate(callId,
|
||||
{
|
||||
{QStringLiteral("lifetime"), lifetime},
|
||||
{QStringLiteral("version"), 1},
|
||||
{QStringLiteral("description"),
|
||||
QJsonObject{{QStringLiteral("type"), answer ? QStringLiteral("answer") : QStringLiteral("offer")}, {QStringLiteral("sdp"), sdp}}},
|
||||
{QStringLiteral("party_id"), partyId},
|
||||
})
|
||||
{
|
||||
QJsonObject metadata;
|
||||
for (const auto &[stream, purpose] : msidToPurpose) {
|
||||
QJsonObject data = {{"purpose", purpose}};
|
||||
metadata[stream] = purpose;
|
||||
}
|
||||
auto content = editJson();
|
||||
content["org.matrix.msc3077.sdp_stream_metadata"] = metadata;
|
||||
editJson()["content"] = content;
|
||||
}
|
||||
|
||||
CallNegotiateEvent::CallNegotiateEvent(const QJsonObject &json)
|
||||
: EventTemplate(json)
|
||||
{
|
||||
}
|
||||
|
||||
QString CallNegotiateEvent::partyId() const
|
||||
{
|
||||
return contentJson()["party_id"].toString();
|
||||
}
|
||||
|
||||
QString CallNegotiateEvent::sdp() const
|
||||
{
|
||||
return contentJson()["description"]["sdp"].toString();
|
||||
}
|
||||
|
||||
QJsonObject CallNegotiateEvent::sdpStreamMetadata() const
|
||||
{
|
||||
return contentJson()["org.matrix.msc3077.sdp_stream_metadata"].toObject();
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <events/callevents.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
|
||||
class CallNegotiateEvent : public EventTemplate<CallNegotiateEvent, CallEvent>
|
||||
{
|
||||
public:
|
||||
QUO_EVENT(CallNegotiateEvent, "m.call.negotiate")
|
||||
|
||||
explicit CallNegotiateEvent(const QJsonObject &obj);
|
||||
|
||||
explicit CallNegotiateEvent(const QString &callId,
|
||||
const QString &partyId,
|
||||
int lifetime,
|
||||
const QString &sdp,
|
||||
bool answer,
|
||||
QVector<std::pair<QString, QString>> msidToPurpose);
|
||||
|
||||
QString partyId() const;
|
||||
QString sdp() const;
|
||||
// TODO make this a struct instead
|
||||
QJsonObject sdpStreamMetadata() const;
|
||||
};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "callparticipant.h"
|
||||
|
||||
NeoChatUser *CallParticipant::user() const
|
||||
{
|
||||
return m_user;
|
||||
}
|
||||
|
||||
bool CallParticipant::hasCamera() const
|
||||
{
|
||||
return m_hasCamera;
|
||||
}
|
||||
|
||||
CallParticipant::CallParticipant(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void CallParticipant::initCamera(QQuickItem *item)
|
||||
{
|
||||
QTimer::singleShot(500, this, [=] {
|
||||
Q_EMIT initialized(item);
|
||||
});
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include "neochatuser.h"
|
||||
|
||||
class QQuickItem;
|
||||
|
||||
class CallParticipant : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(NeoChatUser *user READ user CONSTANT)
|
||||
Q_PROPERTY(bool hasCamera READ hasCamera NOTIFY hasCameraChanged)
|
||||
|
||||
public:
|
||||
NeoChatUser *m_user = nullptr;
|
||||
bool m_hasCamera = false;
|
||||
|
||||
Q_INVOKABLE void initCamera(QQuickItem *item);
|
||||
|
||||
[[nodiscard]] NeoChatUser *user() const;
|
||||
|
||||
[[nodiscard]] bool hasCamera() const;
|
||||
|
||||
explicit CallParticipant(QObject *parent = nullptr);
|
||||
|
||||
Q_SIGNALS:
|
||||
void initialized(QQuickItem *item);
|
||||
void heightChanged();
|
||||
void widthChanged();
|
||||
void hasCameraChanged();
|
||||
};
|
||||
@@ -1,916 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Tobias Fella <fella@posteo.de>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "calldevices.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
#undef GST_USE_UNSTABLE_API
|
||||
|
||||
#include "voiplogging.h"
|
||||
|
||||
#include "audiosources.h"
|
||||
#include "videosources.h"
|
||||
|
||||
#include <qcoro/qcorosignal.h>
|
||||
|
||||
#define private public
|
||||
#include "callsession.h"
|
||||
#undef private
|
||||
#include "callmanager.h"
|
||||
#include <qt_connection_util.h>
|
||||
|
||||
#define STUN_SERVER "stun://turn.matrix.org:3478" // TODO make STUN server configurable
|
||||
|
||||
#define INSTANCE \
|
||||
Q_ASSERT(user_data); \
|
||||
auto instance = static_cast<CallSession *>(user_data);
|
||||
|
||||
GstElement *createElement(const char *type, GstElement *pipe, const char *name = nullptr)
|
||||
{
|
||||
auto element = gst_element_factory_make(type, name);
|
||||
Q_ASSERT_X(element, __FUNCTION__, QStringLiteral("Failed to create element %1 %2").arg(type, name).toLatin1());
|
||||
if (pipe) {
|
||||
gst_bin_add_many(GST_BIN(pipe), element, nullptr);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
GstElement *binGetByName(GstElement *bin, const char *name)
|
||||
{
|
||||
auto element = gst_bin_get_by_name(GST_BIN(bin), name);
|
||||
Q_ASSERT_X(element, __FUNCTION__, QStringLiteral("Failed to get element by name: %1").arg(name).toLatin1());
|
||||
return element;
|
||||
}
|
||||
|
||||
struct KeyFrameRequestData {
|
||||
GstElement *pipe = nullptr;
|
||||
GstElement *decodeBin = nullptr;
|
||||
gint packetsLost = 0;
|
||||
guint timerId = 0;
|
||||
QString statsField;
|
||||
} keyFrameRequestData;
|
||||
|
||||
std::pair<int, int> getResolution(GstPad *pad)
|
||||
{
|
||||
std::pair<int, int> ret;
|
||||
auto caps = gst_pad_get_current_caps(pad);
|
||||
auto structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "width", &ret.first);
|
||||
gst_structure_get_int(structure, "height", &ret.second);
|
||||
gst_caps_unref(caps);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::pair<int, int> getResolution(GstElement *pipe, const gchar *elementName, const gchar *padName)
|
||||
{
|
||||
auto element = binGetByName(pipe, elementName);
|
||||
auto pad = gst_element_get_static_pad(element, padName);
|
||||
auto ret = getResolution(pad);
|
||||
gst_object_unref(pad);
|
||||
gst_object_unref(element);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void setLocalDescription(GstPromise *promise, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCDebug(voip) << "Setting local description";
|
||||
const GstStructure *reply = gst_promise_get_reply(promise);
|
||||
gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
|
||||
GstWebRTCSessionDescription *gstsdp = nullptr;
|
||||
gst_structure_get(reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr);
|
||||
gst_promise_unref(promise);
|
||||
auto webrtcbin = binGetByName(instance->m_pipe, "webrtcbin");
|
||||
Q_ASSERT(gstsdp);
|
||||
g_signal_emit_by_name(webrtcbin, "set-local-description", gstsdp, nullptr);
|
||||
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
|
||||
if (!instance->m_localSdp.isEmpty()) {
|
||||
// This is a renegotiation
|
||||
qWarning() << "emitting renegotiate";
|
||||
Q_EMIT instance->renegotiate(QString(sdp), isAnswer ? QStringLiteral("answer") : QStringLiteral("offer"));
|
||||
}
|
||||
instance->m_localSdp = QString(sdp);
|
||||
g_free(sdp);
|
||||
gst_webrtc_session_description_free(gstsdp);
|
||||
qCDebug(voip) << "Local description set:" << isAnswer;
|
||||
}
|
||||
|
||||
bool contains(std::string_view str1, std::string_view str2)
|
||||
{
|
||||
return std::search(str1.cbegin(),
|
||||
str1.cend(),
|
||||
str2.cbegin(),
|
||||
str2.cend(),
|
||||
[](unsigned char c1, unsigned char c2) {
|
||||
return std::tolower(c1) == std::tolower(c2);
|
||||
})
|
||||
!= str1.cend();
|
||||
}
|
||||
|
||||
void createOffer(GstElement *webrtc, CallSession *session)
|
||||
{
|
||||
// TODO ?!?
|
||||
if (!session->m_localSdp.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
qCWarning(voip) << "Creating Offer";
|
||||
auto promise = gst_promise_new_with_change_func(setLocalDescription, session, nullptr);
|
||||
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
|
||||
}
|
||||
|
||||
void createAnswer(GstPromise *promise, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCDebug(voip) << "Creating Answer";
|
||||
gst_promise_unref(promise);
|
||||
promise = gst_promise_new_with_change_func(setLocalDescription, instance, nullptr);
|
||||
auto webrtcbin = binGetByName(instance->m_pipe, "webrtcbin");
|
||||
g_signal_emit_by_name(webrtcbin, "create-answer", nullptr, promise);
|
||||
}
|
||||
|
||||
bool getMediaAttributes(const GstSDPMessage *sdp, const char *mediaType, const char *encoding, int &payloadType, bool &receiveOnly, bool &sendOnly)
|
||||
{
|
||||
payloadType = -1;
|
||||
receiveOnly = false;
|
||||
sendOnly = false;
|
||||
for (guint mlineIndex = 0; mlineIndex < gst_sdp_message_medias_len(sdp); mlineIndex++) {
|
||||
const GstSDPMedia *media = gst_sdp_message_get_media(sdp, mlineIndex);
|
||||
if (!strcmp(gst_sdp_media_get_media(media), mediaType)) {
|
||||
receiveOnly = gst_sdp_media_get_attribute_val(media, "recvonly") != nullptr;
|
||||
sendOnly = gst_sdp_media_get_attribute_val(media, "sendonly") != nullptr;
|
||||
const gchar *rtpval = nullptr;
|
||||
for (guint n = 0; n == 0 || rtpval; n++) {
|
||||
rtpval = gst_sdp_media_get_attribute_val_n(media, "rtpmap", n);
|
||||
if (rtpval && contains(rtpval, encoding)) {
|
||||
payloadType = QString::fromLatin1(rtpval).toInt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GstWebRTCSessionDescription *parseSDP(const QString &sdp, GstWebRTCSDPType type)
|
||||
{
|
||||
GstSDPMessage *message;
|
||||
gst_sdp_message_new(&message);
|
||||
if (gst_sdp_message_parse_buffer((guint8 *)sdp.toLatin1().data(), sdp.size(), message) == GST_SDP_OK) {
|
||||
return gst_webrtc_session_description_new(type, message);
|
||||
} else {
|
||||
qCCritical(voip) << "Failed to parse remote SDP";
|
||||
gst_sdp_message_free(message);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void addLocalICECandidate(GstElement *webrtc, guint mlineIndex, const gchar *candidate, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(webrtc);
|
||||
INSTANCE
|
||||
// qCWarning(voip) << "Adding local ICE Candidates";
|
||||
instance->m_localCandidates += Candidate{candidate, static_cast<int>(mlineIndex), QString()};
|
||||
}
|
||||
|
||||
void iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pspec);
|
||||
INSTANCE
|
||||
GstWebRTCICEConnectionState newState;
|
||||
g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
|
||||
switch (newState) {
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_NEW:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
|
||||
instance->setState(CallSession::CONNECTING);
|
||||
break;
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
|
||||
instance->setState(CallSession::ICEFAILED);
|
||||
break;
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CONNECTED:
|
||||
instance->setState(CallSession::CONNECTED);
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_COMPLETED:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_DISCONNECTED:
|
||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CLOSED:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GstElement *newAudioSinkChain(GstElement *pipe)
|
||||
{
|
||||
qCWarning(voip) << "New Audio Sink Chain";
|
||||
GstElement *queue = createElement("queue", pipe);
|
||||
GstElement *convert = createElement("audioconvert", pipe);
|
||||
GstElement *resample = createElement("audioresample", pipe);
|
||||
GstElement *sink = createElement("autoaudiosink", pipe);
|
||||
gst_element_link_many(queue, convert, resample, sink, nullptr);
|
||||
gst_element_sync_state_with_parent(queue);
|
||||
gst_element_sync_state_with_parent(convert);
|
||||
gst_element_sync_state_with_parent(resample);
|
||||
gst_element_sync_state_with_parent(sink);
|
||||
return queue;
|
||||
}
|
||||
|
||||
void sendKeyFrameRequest()
|
||||
{
|
||||
auto sinkpad = gst_element_get_static_pad(keyFrameRequestData.decodeBin, "sink");
|
||||
if (!gst_pad_push_event(sinkpad, gst_event_new_custom(GST_EVENT_CUSTOM_UPSTREAM, gst_structure_new_empty("GstForceKeyUnit")))) {
|
||||
qCWarning(voip) << "Keyframe request failed";
|
||||
}
|
||||
gst_object_unref(sinkpad);
|
||||
}
|
||||
|
||||
void onGetStats(GstPromise *promise, gpointer)
|
||||
{
|
||||
auto reply = gst_promise_get_reply(promise);
|
||||
GstStructure *rtpStats;
|
||||
if (!gst_structure_get(reply, keyFrameRequestData.statsField.toLatin1().data(), GST_TYPE_STRUCTURE, &rtpStats, nullptr)) {
|
||||
gst_promise_unref(promise);
|
||||
return;
|
||||
}
|
||||
auto packetsLost = 0;
|
||||
gst_structure_get_int(rtpStats, "packets-lost", &packetsLost);
|
||||
gst_structure_free(rtpStats);
|
||||
gst_promise_unref(promise);
|
||||
if (packetsLost > keyFrameRequestData.packetsLost) {
|
||||
qCWarning(voip) << "inbound video lost packet count:" << packetsLost;
|
||||
keyFrameRequestData.packetsLost = packetsLost;
|
||||
sendKeyFrameRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO port to QTimer?
|
||||
gboolean testPacketLoss(gpointer)
|
||||
{
|
||||
if (!keyFrameRequestData.pipe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto webrtc = binGetByName(keyFrameRequestData.pipe, "webrtcbin");
|
||||
auto promise = gst_promise_new_with_change_func(onGetStats, nullptr, nullptr);
|
||||
g_signal_emit_by_name(webrtc, "get-stats", nullptr, promise);
|
||||
gst_object_unref(webrtc);
|
||||
return true;
|
||||
}
|
||||
|
||||
GstElement *newVideoSinkChain(GstElement *pipe, QQuickItem *quickItem)
|
||||
{
|
||||
Q_ASSERT(pipe);
|
||||
Q_ASSERT(quickItem);
|
||||
qCWarning(voip) << "Creating Video Sink Chain";
|
||||
auto queue = createElement("queue", pipe);
|
||||
auto compositor = createElement("compositor", pipe);
|
||||
auto glupload = createElement("glupload", pipe);
|
||||
auto glcolorconvert = createElement("glcolorconvert", pipe);
|
||||
auto qmlglsink = createElement("qmlglsink", nullptr);
|
||||
auto glsinkbin = createElement("glsinkbin", pipe);
|
||||
g_object_set(qmlglsink, "widget", quickItem, nullptr);
|
||||
g_object_set(glsinkbin, "sink", qmlglsink, nullptr);
|
||||
gst_element_link_many(queue, compositor, glupload, glcolorconvert, glsinkbin, nullptr);
|
||||
gst_element_sync_state_with_parent(queue);
|
||||
gst_element_sync_state_with_parent(compositor);
|
||||
gst_element_sync_state_with_parent(glupload);
|
||||
gst_element_sync_state_with_parent(glcolorconvert);
|
||||
gst_element_sync_state_with_parent(glsinkbin);
|
||||
return queue;
|
||||
}
|
||||
|
||||
static GstPadProbeReturn pad_cb(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pad);
|
||||
// auto stream = static_cast<VideoStream *>(user_data);
|
||||
auto event = GST_PAD_PROBE_INFO_EVENT(info);
|
||||
if (GST_EVENT_CAPS == GST_EVENT_TYPE(event)) {
|
||||
GstCaps *caps = gst_caps_new_any();
|
||||
int width, height;
|
||||
gst_event_parse_caps(event, &caps);
|
||||
auto structure = gst_caps_get_structure(caps, 0);
|
||||
gst_structure_get_int(structure, "width", &width);
|
||||
gst_structure_get_int(structure, "height", &height);
|
||||
// stream->setWidth(width);
|
||||
// stream->setHeight(height);
|
||||
// TODO needed?
|
||||
}
|
||||
return GST_PAD_PROBE_OK;
|
||||
}
|
||||
|
||||
void linkNewPad(GstElement *decodeBin, GstPad *newpad, gpointer user_data)
|
||||
{
|
||||
INSTANCE
|
||||
qCWarning(voip) << "Linking New Pad";
|
||||
auto sinkpad = gst_element_get_static_pad(decodeBin, "sink");
|
||||
auto sinkcaps = gst_pad_get_current_caps(sinkpad);
|
||||
auto structure = gst_caps_get_structure(sinkcaps, 0);
|
||||
|
||||
gchar *mediaType = nullptr;
|
||||
guint ssrc = 0;
|
||||
gst_structure_get(structure, "media", G_TYPE_STRING, &mediaType, "ssrc", G_TYPE_UINT, &ssrc, nullptr);
|
||||
gst_caps_unref(sinkcaps);
|
||||
gst_object_unref(sinkpad);
|
||||
|
||||
GstElement *queue = nullptr;
|
||||
if (!strcmp(mediaType, "audio")) {
|
||||
qCWarning(voip) << "Receiving audio stream";
|
||||
queue = newAudioSinkChain(instance->m_pipe);
|
||||
} else if (!strcmp(mediaType, "video")) {
|
||||
qCWarning(voip) << "Receiving video stream";
|
||||
auto fake = createElement("fakesink", instance->m_pipe);
|
||||
auto selector = createElement("output-selector", instance->m_pipe);
|
||||
auto selectorSink = gst_element_get_static_pad(selector, "sink");
|
||||
auto selectorSrc1 = gst_element_request_pad_simple(selector, "src_%u");
|
||||
gst_pad_link(newpad, selectorSink);
|
||||
auto fakepad = gst_element_get_static_pad(fake, "sink");
|
||||
gst_pad_link(selectorSrc1, fakepad);
|
||||
g_object_set(selector, "active-pad", selectorSrc1, nullptr);
|
||||
|
||||
auto msid = instance->ssrcToMsid[ssrc];
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, stream, nullptr);
|
||||
auto manager = dynamic_cast<CallManager *>(instance->parent());
|
||||
auto participants = manager->callParticipants();
|
||||
auto user = dynamic_cast<NeoChatUser *>(manager->room()->user(instance->msidToUserId[msid]));
|
||||
participants->setHasCamera(user, true);
|
||||
|
||||
auto participant = participants->callParticipantForUser(user);
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, nullptr, nullptr);
|
||||
connectSingleShot(participant, &CallParticipant::initialized, instance, [=](QQuickItem *item) {
|
||||
gst_pad_unlink(newpad, fakepad);
|
||||
auto queue = newVideoSinkChain(instance->m_pipe, item);
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto selectorSrc = gst_element_request_pad_simple(selector, "src_%u");
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(selectorSrc, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
g_object_set(selector, "active-pad", selectorSrc, nullptr);
|
||||
instance->setState(CallSession::CONNECTED);
|
||||
keyFrameRequestData.pipe = instance->m_pipe;
|
||||
keyFrameRequestData.decodeBin = decodeBin;
|
||||
keyFrameRequestData.timerId = g_timeout_add_seconds(3, testPacketLoss, nullptr);
|
||||
keyFrameRequestData.statsField = QStringLiteral("rtp-inbound-stream-stats_") + QString::number(ssrc);
|
||||
gst_object_unref(queuepad);
|
||||
g_free(mediaType);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
g_free(mediaType);
|
||||
qCWarning(voip) << "Unknown pad type:" << GST_PAD_NAME(newpad);
|
||||
return;
|
||||
}
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(newpad, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
gst_object_unref(queuepad);
|
||||
g_free(mediaType);
|
||||
}
|
||||
|
||||
void setWaitForKeyFrame(GstBin *decodeBin, GstElement *element, gpointer)
|
||||
{
|
||||
Q_UNUSED(decodeBin);
|
||||
if (!strcmp(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(gst_element_get_factory(element))), "rtpvp8depay")) {
|
||||
g_object_set(element, "wait-for-keyframe", TRUE, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void addDecodeBin(GstElement *webrtc, GstPad *newpad, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(webrtc);
|
||||
if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC) {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE
|
||||
|
||||
auto decodeBin = createElement("decodebin", instance->m_pipe);
|
||||
// Investigate hardware, see nheko source
|
||||
g_object_set(decodeBin, "force-sw-decoders", TRUE, nullptr);
|
||||
g_signal_connect(decodeBin, "pad-added", G_CALLBACK(linkNewPad), instance);
|
||||
g_signal_connect(decodeBin, "element-added", G_CALLBACK(setWaitForKeyFrame), nullptr);
|
||||
gst_element_sync_state_with_parent(decodeBin);
|
||||
auto sinkpad = gst_element_get_static_pad(decodeBin, "sink");
|
||||
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad))) {
|
||||
// TODO: Error handling
|
||||
qCWarning(voip) << "Unable to link decodebin";
|
||||
}
|
||||
gst_object_unref(sinkpad);
|
||||
}
|
||||
|
||||
void iceGatheringStateChanged(GstElement *webrtc, GParamSpec *pspec, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(pspec);
|
||||
INSTANCE
|
||||
|
||||
GstWebRTCICEGatheringState newState;
|
||||
g_object_get(webrtc, "ice-gathering-state", &newState, nullptr);
|
||||
if (newState == GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE) {
|
||||
qCWarning(voip) << "GstWebRTCICEGatheringState -> Complete";
|
||||
if (instance->m_isOffering) {
|
||||
Q_EMIT instance->offerCreated(instance->m_localSdp, instance->m_localCandidates);
|
||||
instance->setState(CallSession::OFFERSENT);
|
||||
} else {
|
||||
Q_EMIT instance->answerCreated(instance->m_localSdp, instance->m_localCandidates);
|
||||
instance->setState(CallSession::ANSWERSENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gboolean newBusMessage(GstBus *bus, GstMessage *msg, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus);
|
||||
INSTANCE
|
||||
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
case GST_MESSAGE_EOS:
|
||||
qCWarning(voip) << "End of stream";
|
||||
// TODO: Error handling
|
||||
instance->end();
|
||||
break;
|
||||
case GST_MESSAGE_ERROR:
|
||||
GError *error;
|
||||
gchar *debug;
|
||||
gst_message_parse_error(msg, &error, &debug);
|
||||
qCWarning(voip) << "Error from element:" << GST_OBJECT_NAME(msg->src) << error->message;
|
||||
// TODO: Error handling
|
||||
g_clear_error(&error);
|
||||
g_free(debug);
|
||||
instance->end();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
CallSession::CallSession(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void CallSession::acceptAnswer(const QString &sdp, const QVector<Candidate> &candidates, const QString &userId)
|
||||
{
|
||||
qCDebug(voip) << "Accepting Answer";
|
||||
if (m_state != CallSession::OFFERSENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
|
||||
if (!answer) {
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
acceptCandidates(candidates);
|
||||
|
||||
setRemoteDescription(answer, userId);
|
||||
}
|
||||
|
||||
void CallSession::setRemoteDescription(GstWebRTCSessionDescription *remote, const QString &userId, GstPromise *promise)
|
||||
{
|
||||
GstElement *webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
auto sdp = remote->sdp;
|
||||
for (guint i = 0; i < gst_sdp_message_medias_len(sdp); i++) {
|
||||
auto media = gst_sdp_message_get_media(sdp, i);
|
||||
QList<uint32_t> ssrcs;
|
||||
QString msid;
|
||||
for (guint j = 0; j < gst_sdp_media_attributes_len(media); j++) {
|
||||
auto attribute = gst_sdp_media_get_attribute(media, j);
|
||||
if (!strcmp(attribute->key, "ssrc")) {
|
||||
ssrcs += QString(attribute->value).split(" ")[0].toUInt();
|
||||
}
|
||||
if (!strcmp(attribute->key, "msid")) {
|
||||
msid = QString(attribute->value).split(" ")[0];
|
||||
}
|
||||
}
|
||||
for (const auto &ssrc : ssrcs) {
|
||||
ssrcToMsid[ssrc] = msid;
|
||||
}
|
||||
msidToUserId[msid] = userId;
|
||||
}
|
||||
g_signal_emit_by_name(webrtcbin, "set-remote-description", remote, promise);
|
||||
}
|
||||
|
||||
void CallSession::renegotiateOffer(const QString &_offer, const QString &userId, bool answer)
|
||||
{
|
||||
GstWebRTCSessionDescription *sdp = parseSDP(_offer, answer ? GST_WEBRTC_SDP_TYPE_ANSWER : GST_WEBRTC_SDP_TYPE_OFFER);
|
||||
if (!sdp) {
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
GstElement *webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
|
||||
setRemoteDescription(sdp, userId);
|
||||
qWarning() << "answer:" << answer;
|
||||
if (!answer) {
|
||||
GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, this, nullptr);
|
||||
g_signal_emit_by_name(webrtcbin, "create-answer", nullptr, promise);
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::acceptOffer(const QString &sdp, const QVector<Candidate> remoteCandidates, const QString &userId)
|
||||
{
|
||||
Q_ASSERT(!sdp.isEmpty());
|
||||
Q_ASSERT(!remoteCandidates.isEmpty());
|
||||
qCDebug(voip) << "Accepting offer";
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
m_isOffering = false;
|
||||
|
||||
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
|
||||
if (!offer) {
|
||||
qCCritical(voip) << "Not an offer";
|
||||
return;
|
||||
}
|
||||
|
||||
int opusPayloadType;
|
||||
bool receiveOnly;
|
||||
bool sendOnly;
|
||||
if (getMediaAttributes(offer->sdp, "audio", "opus", opusPayloadType, receiveOnly, sendOnly)) {
|
||||
if (opusPayloadType == -1) {
|
||||
qCCritical(voip) << "No OPUS in offer";
|
||||
gst_webrtc_session_description_free(offer);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
qCCritical(voip) << "No audio in offer";
|
||||
gst_webrtc_session_description_free(offer);
|
||||
return;
|
||||
}
|
||||
startPipeline();
|
||||
|
||||
QThread::msleep(1000); // ?
|
||||
|
||||
acceptCandidates(remoteCandidates);
|
||||
|
||||
auto promise = gst_promise_new_with_change_func(createAnswer, this, nullptr);
|
||||
setRemoteDescription(offer, userId, promise);
|
||||
gst_webrtc_session_description_free(offer);
|
||||
}
|
||||
|
||||
void CallSession::createCall()
|
||||
{
|
||||
qCDebug(voip) << "Creating call";
|
||||
m_isOffering = true;
|
||||
startPipeline();
|
||||
}
|
||||
|
||||
void CallSession::startPipeline()
|
||||
{
|
||||
qCDebug(voip) << "Starting Pipeline";
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
return;
|
||||
}
|
||||
m_state = CallSession::INITIATING;
|
||||
Q_EMIT stateChanged();
|
||||
|
||||
createPipeline();
|
||||
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
Q_ASSERT(webrtcbin);
|
||||
if (false /*TODO: CHECK USE STUN*/) {
|
||||
qCDebug(voip) << "Setting STUN server:" << STUN_SERVER;
|
||||
g_object_set(webrtcbin, "stun-server", STUN_SERVER, nullptr);
|
||||
}
|
||||
|
||||
for (const auto &uri : m_turnServers) {
|
||||
qCDebug(voip) << "Setting turn server:" << uri;
|
||||
gboolean udata;
|
||||
g_signal_emit_by_name(webrtcbin, "add-turn-server", uri.toLatin1().data(), (gpointer)(&udata));
|
||||
}
|
||||
|
||||
if (m_turnServers.empty()) {
|
||||
qCWarning(voip) << "No TURN servers provided";
|
||||
}
|
||||
|
||||
if (m_isOffering) {
|
||||
g_signal_connect(webrtcbin, "on-negotiation-needed", G_CALLBACK(::createOffer), this);
|
||||
}
|
||||
|
||||
g_signal_connect(webrtcbin, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), this);
|
||||
g_signal_connect(webrtcbin, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), this);
|
||||
|
||||
gst_element_set_state(m_pipe, GST_STATE_READY);
|
||||
g_signal_connect(webrtcbin, "pad-added", G_CALLBACK(addDecodeBin), this);
|
||||
|
||||
g_signal_connect(webrtcbin, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), this);
|
||||
gst_object_unref(webrtcbin);
|
||||
|
||||
GstStateChangeReturn ret = gst_element_set_state(m_pipe, GST_STATE_PLAYING);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||
// TODO: Error handling
|
||||
qCCritical(voip) << "Unable to start pipeline";
|
||||
end();
|
||||
return;
|
||||
}
|
||||
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipe));
|
||||
m_busWatchId = gst_bus_add_watch(bus, newBusMessage, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
m_state = CallSession::INITIATED;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
|
||||
void CallSession::end()
|
||||
{
|
||||
qCDebug(voip) << "Ending Call";
|
||||
if (m_pipe) {
|
||||
gst_element_set_state(m_pipe, GST_STATE_NULL);
|
||||
gst_object_unref(m_pipe);
|
||||
m_pipe = nullptr;
|
||||
keyFrameRequestData.pipe = nullptr;
|
||||
if (m_busWatchId) {
|
||||
g_source_remove(m_busWatchId);
|
||||
m_busWatchId = 0;
|
||||
}
|
||||
}
|
||||
if (m_state != CallSession::DISCONNECTED) {
|
||||
m_state = CallSession::DISCONNECTED;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::createPipeline()
|
||||
{
|
||||
qCWarning(voip) << "Creating Pipeline";
|
||||
auto device = AudioSources::instance().currentDevice();
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
m_pipe = gst_pipeline_new(nullptr);
|
||||
auto source = gst_device_create_element(device, nullptr);
|
||||
auto volume = createElement("volume", m_pipe, "srclevel");
|
||||
auto convert = createElement("audioconvert", m_pipe);
|
||||
auto resample = createElement("audioresample", m_pipe);
|
||||
auto queue1 = createElement("queue", m_pipe);
|
||||
auto opusenc = createElement("opusenc", m_pipe);
|
||||
auto rtp = createElement("rtpopuspay", m_pipe);
|
||||
auto queue2 = createElement("queue", m_pipe);
|
||||
auto capsfilter = createElement("capsfilter", m_pipe);
|
||||
|
||||
auto rtpcaps = gst_caps_new_simple("application/x-rtp",
|
||||
"media",
|
||||
G_TYPE_STRING,
|
||||
"audio",
|
||||
"encoding-name",
|
||||
G_TYPE_STRING,
|
||||
"OPUS",
|
||||
"payload",
|
||||
G_TYPE_INT,
|
||||
OPUS_PAYLOAD_TYPE,
|
||||
nullptr);
|
||||
Q_ASSERT(rtpcaps);
|
||||
g_object_set(capsfilter, "caps", rtpcaps, nullptr);
|
||||
gst_caps_unref(rtpcaps);
|
||||
|
||||
auto webrtcbin = createElement("webrtcbin", m_pipe, "webrtcbin");
|
||||
g_object_set(webrtcbin, "bundle-policy", GST_WEBRTC_BUNDLE_POLICY_MAX_BUNDLE, nullptr);
|
||||
|
||||
gst_bin_add_many(GST_BIN(m_pipe), source, nullptr);
|
||||
|
||||
if (!gst_element_link_many(source, volume, convert, resample, queue1, opusenc, rtp, queue2, capsfilter, webrtcbin, nullptr)) {
|
||||
qCCritical(voip) << "Failed to link pipeline";
|
||||
// TODO propagate errors up and end call
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CallSession::toggleCamera()
|
||||
{
|
||||
// TODO do this only once
|
||||
static bool inited = false;
|
||||
if (!inited) {
|
||||
addVideoPipeline();
|
||||
inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool CallSession::addVideoPipeline()
|
||||
{
|
||||
qCDebug(voip) << "Adding Video Pipeline";
|
||||
auto videoconvert = createElement("videoconvertscale", m_pipe);
|
||||
auto tee = createElement("tee", m_pipe);
|
||||
auto device = VideoSources::instance().currentDevice();
|
||||
auto deviceCaps = device->caps[VideoSources::instance().capsIndex()];
|
||||
int width = deviceCaps.width;
|
||||
int height = deviceCaps.height;
|
||||
int framerate = deviceCaps.framerates.back();
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
auto camera = gst_device_create_element(device->device, nullptr);
|
||||
gst_bin_add_many(GST_BIN(m_pipe), camera, nullptr);
|
||||
|
||||
auto caps =
|
||||
gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, width, "height", G_TYPE_INT, height, "framerate", GST_TYPE_FRACTION, framerate, 1, nullptr);
|
||||
auto camerafilter = createElement("capsfilter", m_pipe);
|
||||
g_object_set(camerafilter, "caps", caps, nullptr);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
gst_element_link(camera, videoconvert);
|
||||
|
||||
if (!gst_element_link_many(videoconvert, camerafilter, nullptr)) {
|
||||
qCWarning(voip) << "Failed to link camera elements";
|
||||
// TODO: Error handling
|
||||
return false;
|
||||
}
|
||||
if (!gst_element_link(camerafilter, tee)) {
|
||||
qCWarning(voip) << "Failed to link camerafilter -> tee";
|
||||
// TODO: Error handling
|
||||
return false;
|
||||
}
|
||||
|
||||
auto queue = createElement("queue", m_pipe);
|
||||
g_object_set(queue, "leaky", true, nullptr);
|
||||
auto vp8enc = createElement("vp8enc", m_pipe);
|
||||
g_object_set(vp8enc, "deadline", 1, nullptr);
|
||||
g_object_set(vp8enc, "error-resilient", 1, nullptr);
|
||||
auto rtpvp8pay = createElement("rtpvp8pay", m_pipe);
|
||||
auto rtpqueue = createElement("queue", m_pipe);
|
||||
auto rtpcapsfilter = createElement("capsfilter", m_pipe);
|
||||
auto rtpcaps = gst_caps_new_simple("application/x-rtp",
|
||||
"media",
|
||||
G_TYPE_STRING,
|
||||
"video",
|
||||
"encoding-name",
|
||||
G_TYPE_STRING,
|
||||
"VP8",
|
||||
"payload",
|
||||
G_TYPE_INT,
|
||||
VP8_PAYLOAD_TYPE,
|
||||
nullptr);
|
||||
g_object_set(rtpcapsfilter, "caps", rtpcaps, nullptr);
|
||||
gst_caps_unref(rtpcaps);
|
||||
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
if (!gst_element_link_many(tee, queue, vp8enc, rtpvp8pay, rtpqueue, rtpcapsfilter, webrtcbin, nullptr)) {
|
||||
qCCritical(voip) << "Failed to link rtp video elements";
|
||||
gst_object_unref(webrtcbin);
|
||||
return false;
|
||||
}
|
||||
auto promise = gst_promise_new_with_change_func(setLocalDescription, this, nullptr);
|
||||
g_signal_emit_by_name(webrtcbin, "create-offer", nullptr, promise);
|
||||
|
||||
gst_object_unref(webrtcbin);
|
||||
|
||||
auto newpad = gst_element_request_pad_simple(tee, "src_%u");
|
||||
Q_ASSERT(newpad);
|
||||
|
||||
auto fake = createElement("fakesink", m_pipe);
|
||||
auto selector = createElement("output-selector", m_pipe);
|
||||
auto selectorSink = gst_element_get_static_pad(selector, "sink");
|
||||
auto selectorSrc1 = gst_element_request_pad_simple(selector, "src_%u");
|
||||
gst_pad_link(newpad, selectorSink);
|
||||
auto fakepad = gst_element_get_static_pad(fake, "sink");
|
||||
gst_pad_link(selectorSrc1, fakepad);
|
||||
g_object_set(selector, "active-pad", selectorSrc1, nullptr);
|
||||
|
||||
// gst_pad_add_probe(newpad, GST_PAD_PROBE_TYPE_EVENT_BOTH, pad_cb, stream, nullptr);
|
||||
auto manager = dynamic_cast<CallManager *>(parent());
|
||||
auto participants = manager->callParticipants();
|
||||
auto user = dynamic_cast<NeoChatUser *>(manager->room()->localUser());
|
||||
participants->setHasCamera(user, true);
|
||||
|
||||
connectSingleShot(participants->callParticipantForUser(user), &CallParticipant::initialized, this, [=](QQuickItem *item) {
|
||||
gst_pad_unlink(newpad, fakepad);
|
||||
Q_ASSERT(item);
|
||||
|
||||
auto queue = newVideoSinkChain(m_pipe, item);
|
||||
Q_ASSERT(queue);
|
||||
auto queuepad = gst_element_get_static_pad(queue, "sink");
|
||||
Q_ASSERT(queuepad);
|
||||
auto selectorSrc = gst_element_request_pad_simple(selector, "src_%u");
|
||||
Q_ASSERT(selectorSrc);
|
||||
auto ok = GST_PAD_LINK_SUCCESSFUL(gst_pad_link(selectorSrc, queuepad));
|
||||
Q_ASSERT(ok);
|
||||
g_object_set(selector, "active-pad", selectorSrc, nullptr);
|
||||
gst_object_unref(queuepad);
|
||||
gst_element_set_state(m_pipe, GST_STATE_READY); // TODO experimental
|
||||
gst_element_set_state(m_pipe, GST_STATE_PLAYING); // TODO experimental
|
||||
GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(m_pipe), GST_DEBUG_GRAPH_SHOW_ALL, "foo");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void CallSession::setTurnServers(QStringList servers)
|
||||
{
|
||||
qCDebug(voip) << "Setting Turn Servers";
|
||||
qWarning() << "TURN SERVERS" << servers;
|
||||
m_turnServers = servers;
|
||||
}
|
||||
|
||||
void CallSession::acceptCandidates(const QVector<Candidate> &candidates)
|
||||
{
|
||||
qCDebug(voip) << "Accepting ICE Candidates";
|
||||
auto webrtcbin = binGetByName(m_pipe, "webrtcbin");
|
||||
for (const auto &c : candidates) {
|
||||
qCDebug(voip) << "Remote candidate:" << c.candidate << c.sdpMLineIndex;
|
||||
g_signal_emit_by_name(webrtcbin, "add-ice-candidate", c.sdpMLineIndex, c.candidate.toLatin1().data());
|
||||
}
|
||||
}
|
||||
|
||||
QStringList CallSession::missingPlugins()
|
||||
{
|
||||
GstRegistry *registry = gst_registry_get();
|
||||
static const QVector<QString> videoPlugins = {
|
||||
QLatin1String("compositor"),
|
||||
QLatin1String("opengl"),
|
||||
QLatin1String("qmlgl"),
|
||||
QLatin1String("rtp"),
|
||||
QLatin1String("videoconvertscale"),
|
||||
QLatin1String("vpx"),
|
||||
};
|
||||
static const QVector<QString> audioPlugins = {
|
||||
QStringLiteral("audioconvert"),
|
||||
QStringLiteral("audioresample"),
|
||||
QStringLiteral("autodetect"),
|
||||
QStringLiteral("dtls"),
|
||||
QStringLiteral("nice"),
|
||||
QStringLiteral("opus"),
|
||||
QStringLiteral("playback"),
|
||||
QStringLiteral("rtpmanager"),
|
||||
QStringLiteral("srtp"),
|
||||
QStringLiteral("volume"),
|
||||
QStringLiteral("webrtc"),
|
||||
};
|
||||
QStringList missingPlugins;
|
||||
for (const auto &pluginName : videoPlugins + audioPlugins) {
|
||||
auto plugin = gst_registry_find_plugin(registry, pluginName.toLatin1().data());
|
||||
if (!plugin) {
|
||||
missingPlugins << pluginName;
|
||||
}
|
||||
gst_object_unref(plugin);
|
||||
}
|
||||
return missingPlugins;
|
||||
}
|
||||
|
||||
void CallSession::setMuted(bool muted)
|
||||
{
|
||||
const auto srclevel = binGetByName(m_pipe, "srclevel");
|
||||
g_object_set(srclevel, "mute", muted, nullptr);
|
||||
gst_object_unref(srclevel);
|
||||
Q_EMIT mutedChanged();
|
||||
}
|
||||
|
||||
bool CallSession::muted() const
|
||||
{
|
||||
if (m_state < CallSession::CONNECTING) {
|
||||
return false;
|
||||
}
|
||||
if (!m_pipe) {
|
||||
return false;
|
||||
}
|
||||
const auto srclevel = binGetByName(m_pipe, "srclevel");
|
||||
bool muted;
|
||||
if (!srclevel) {
|
||||
return false;
|
||||
}
|
||||
g_object_get(srclevel, "mute", &muted, nullptr);
|
||||
// gst_object_unref(srclevel); //TODO why does this crash?
|
||||
return muted;
|
||||
}
|
||||
|
||||
CallSession *
|
||||
CallSession::acceptCall(const QString &sdp, const QVector<Candidate> &candidates, const QStringList &turnUris, const QString &userId, QObject *parent)
|
||||
{
|
||||
auto instance = new CallSession(parent);
|
||||
instance->setTurnServers(turnUris);
|
||||
instance->acceptOffer(sdp, candidates, userId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
CallSession *CallSession::startCall(const QStringList &turnUris, QObject *parent)
|
||||
{
|
||||
auto instance = new CallSession(parent);
|
||||
|
||||
instance->setTurnServers(turnUris);
|
||||
instance->createCall();
|
||||
return instance;
|
||||
}
|
||||
|
||||
CallSession::State CallSession::state() const
|
||||
{
|
||||
return m_state;
|
||||
}
|
||||
|
||||
void CallSession::setState(CallSession::State state)
|
||||
{
|
||||
qCWarning(voip) << "Setting state" << state;
|
||||
m_state = state;
|
||||
Q_EMIT stateChanged();
|
||||
}
|
||||
|
||||
void CallSession::setMetadata(QJsonObject metadata)
|
||||
{
|
||||
m_metadata = metadata;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Nheko Contributors
|
||||
// SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Tobias Fella <fella@posteo.de>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QMetaType>
|
||||
#include <QObject>
|
||||
#include <QQuickItem>
|
||||
#include <QString>
|
||||
#include <variant>
|
||||
#define GST_USE_UNSTABLE_API
|
||||
#include <gst/webrtc/webrtc.h>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#define OPUS_PAYLOAD_TYPE 111
|
||||
#define VP8_PAYLOAD_TYPE 96
|
||||
|
||||
class CallDevices;
|
||||
class VideoStream;
|
||||
|
||||
struct Candidate {
|
||||
QString candidate;
|
||||
int sdpMLineIndex;
|
||||
QString sdpMid;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Candidate)
|
||||
Q_DECLARE_METATYPE(QVector<Candidate>)
|
||||
|
||||
class CallSession : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum State {
|
||||
DISCONNECTED,
|
||||
ICEFAILED,
|
||||
INITIATING,
|
||||
INITIATED,
|
||||
OFFERSENT,
|
||||
ANSWERSENT,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
};
|
||||
Q_ENUM(State);
|
||||
|
||||
Q_PROPERTY(CallSession::State state READ state NOTIFY stateChanged)
|
||||
Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged)
|
||||
|
||||
// For outgoing calls
|
||||
static CallSession *startCall(const QStringList &turnUris, QObject *parent = nullptr);
|
||||
void acceptAnswer(const QString &sdp, const QVector<Candidate> &candidates, const QString &parent);
|
||||
|
||||
// For incoming calls
|
||||
static CallSession *
|
||||
acceptCall(const QString &sdp, const QVector<Candidate> &candidates, const QStringList &turnUris, const QString &userId, QObject *parent = nullptr);
|
||||
|
||||
void end();
|
||||
|
||||
void renegotiateOffer(const QString &offer, const QString &userId, bool answer);
|
||||
void setTurnServers(QStringList servers);
|
||||
|
||||
static QStringList missingPlugins();
|
||||
|
||||
CallSession::State state() const;
|
||||
|
||||
void toggleCamera();
|
||||
bool muted() const;
|
||||
void setMuted(bool muted);
|
||||
void setMetadata(QJsonObject metadata);
|
||||
void acceptCandidates(const QVector<Candidate> &candidates);
|
||||
|
||||
QMap<QString, QString> msidToUserId;
|
||||
Q_SIGNALS:
|
||||
void stateChanged();
|
||||
void offerCreated(const QString &sdp, const QVector<Candidate> &candidates);
|
||||
|
||||
void answerCreated(const QString &sdp, const QVector<Candidate> &candidates);
|
||||
|
||||
void mutedChanged();
|
||||
void newVideoStream(VideoStream *stream);
|
||||
|
||||
void renegotiate(QString sdp, const QString &type);
|
||||
|
||||
private:
|
||||
CallSession(QObject *parent = nullptr);
|
||||
void acceptOffer(const QString &sdp, const QVector<Candidate> remoteCandidates, const QString &userId);
|
||||
void createCall();
|
||||
|
||||
void setRemoteDescription(GstWebRTCSessionDescription *remote, const QString &userId, GstPromise *promise = nullptr);
|
||||
void startPipeline();
|
||||
void createPipeline();
|
||||
bool addVideoPipeline();
|
||||
|
||||
void setState(CallSession::State state);
|
||||
GstPad *m_activePad;
|
||||
GstElement *m_inputSelector;
|
||||
CallSession::State m_state = CallSession::DISCONNECTED;
|
||||
unsigned int m_busWatchId = 0;
|
||||
QStringList m_turnServers;
|
||||
QVector<Candidate> m_localCandidates;
|
||||
QString m_localSdp;
|
||||
GstElement *m_pipe = nullptr;
|
||||
bool m_isOffering = false;
|
||||
QMap<int, QString> ssrcToMsid;
|
||||
QJsonObject m_metadata;
|
||||
GstPad *m_inactivePad;
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "devicemonitor.h"
|
||||
#include "voiplogging.h"
|
||||
#include <QTimer>
|
||||
|
||||
QDebug operator<<(QDebug dbg, const GstStructure *props)
|
||||
{
|
||||
QDebugStateSaver saver(dbg);
|
||||
auto asStr = gst_structure_to_string(props);
|
||||
dbg << asStr;
|
||||
g_free(asStr);
|
||||
return dbg;
|
||||
}
|
||||
|
||||
static gboolean deviceCallback(GstBus *bus, GstMessage *message, gpointer user_data)
|
||||
{
|
||||
Q_UNUSED(bus);
|
||||
auto monitor = static_cast<DeviceMonitor *>(user_data);
|
||||
return monitor->callback(message);
|
||||
}
|
||||
|
||||
DeviceMonitor::DeviceMonitor()
|
||||
: QObject()
|
||||
{
|
||||
QTimer::singleShot(0, this, &DeviceMonitor::init);
|
||||
}
|
||||
|
||||
void DeviceMonitor::init()
|
||||
{
|
||||
if (m_monitor) {
|
||||
return;
|
||||
}
|
||||
m_monitor = gst_device_monitor_new();
|
||||
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
|
||||
gst_device_monitor_add_filter(m_monitor, "Audio/Source", caps);
|
||||
|
||||
gst_caps_unref(caps);
|
||||
caps = gst_caps_new_empty_simple("video/x-raw");
|
||||
gst_device_monitor_add_filter(m_monitor, "Video/Source", caps);
|
||||
gst_caps_unref(caps);
|
||||
|
||||
GstBus *bus = gst_device_monitor_get_bus(m_monitor);
|
||||
gst_bus_add_watch(bus, deviceCallback, this);
|
||||
gst_object_unref(bus);
|
||||
|
||||
if (!gst_device_monitor_start(m_monitor)) {
|
||||
qWarning() << "Failed to start device monitor";
|
||||
}
|
||||
}
|
||||
|
||||
QVector<AudioSource *> DeviceMonitor::audioSources() const
|
||||
{
|
||||
return m_audioSources;
|
||||
}
|
||||
|
||||
QVector<VideoSource *> DeviceMonitor::videoSources() const
|
||||
{
|
||||
return m_videoSources;
|
||||
}
|
||||
|
||||
void DeviceMonitor::handleVideoSource(GstDevice *device)
|
||||
{
|
||||
auto source = new VideoSource();
|
||||
auto title = gst_device_get_display_name(device);
|
||||
source->title = QString(title);
|
||||
g_free(title);
|
||||
source->device = device;
|
||||
|
||||
auto caps = gst_device_get_caps(device);
|
||||
auto size = gst_caps_get_size(caps);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
VideoCap videoCap;
|
||||
GstStructure *cap = gst_caps_get_structure(caps, i);
|
||||
const gchar *name = gst_structure_get_name(cap);
|
||||
if (strcmp(name, "video/x-raw")) {
|
||||
// TODO g_free(name);
|
||||
continue;
|
||||
}
|
||||
// TODO g_free(name);
|
||||
gst_structure_get(cap, "width", G_TYPE_INT, &videoCap.width, "height", G_TYPE_INT, &videoCap.height, nullptr);
|
||||
const auto framerate = gst_structure_get_value(cap, "framerate");
|
||||
if (GST_VALUE_HOLDS_FRACTION(framerate)) {
|
||||
auto numerator = gst_value_get_fraction_numerator(framerate);
|
||||
auto denominator = gst_value_get_fraction_denominator(framerate);
|
||||
videoCap.framerates += (float)numerator / denominator;
|
||||
}
|
||||
// unref cap?
|
||||
source->caps += videoCap;
|
||||
}
|
||||
m_videoSources += source;
|
||||
Q_EMIT videoSourceAdded();
|
||||
}
|
||||
|
||||
void DeviceMonitor::handleAudioSource(GstDevice *device)
|
||||
{
|
||||
auto source = new AudioSource();
|
||||
auto title = gst_device_get_display_name(device);
|
||||
source->title = QString(title);
|
||||
g_free(title);
|
||||
|
||||
GstStructure *props = gst_device_get_properties(device);
|
||||
gboolean isDefault = false;
|
||||
if (gst_structure_has_field(props, "is-default")) {
|
||||
gst_structure_get_boolean(props, "is-default", &isDefault);
|
||||
}
|
||||
gst_structure_free(props);
|
||||
source->isDefault = isDefault;
|
||||
|
||||
source->device = device;
|
||||
m_audioSources += source;
|
||||
Q_EMIT audioSourceAdded();
|
||||
}
|
||||
|
||||
bool DeviceMonitor::callback(GstMessage *message)
|
||||
{
|
||||
GstDevice *device;
|
||||
switch (GST_MESSAGE_TYPE(message)) {
|
||||
case GST_MESSAGE_DEVICE_ADDED: {
|
||||
gst_message_parse_device_added(message, &device);
|
||||
auto name = gst_device_get_display_name(device);
|
||||
auto props = gst_device_get_properties(device);
|
||||
qCDebug(voip) << name << props;
|
||||
gst_structure_free(props);
|
||||
if (gst_device_has_classes(device, "Video/Source")) {
|
||||
handleVideoSource(device);
|
||||
} else if (gst_device_has_classes(device, "Audio/Source")) {
|
||||
handleAudioSource(device);
|
||||
}
|
||||
g_free(name);
|
||||
gst_object_unref(device);
|
||||
break;
|
||||
}
|
||||
case GST_MESSAGE_DEVICE_REMOVED: {
|
||||
gst_message_parse_device_removed(message, &device);
|
||||
auto name = gst_device_get_display_name(device);
|
||||
auto props = gst_device_get_properties(device);
|
||||
qCDebug(voip) << name << props;
|
||||
if (gst_device_has_classes(device, "Video/Source")) {
|
||||
m_videoSources.erase(std::remove_if(m_videoSources.begin(),
|
||||
m_videoSources.end(),
|
||||
[name](auto d) {
|
||||
return d->title == QString(name);
|
||||
}),
|
||||
m_videoSources.end());
|
||||
Q_EMIT videoSourceRemoved();
|
||||
} else if (gst_device_has_classes(device, "Audio/Source")) {
|
||||
m_audioSources.erase(std::remove_if(m_audioSources.begin(),
|
||||
m_audioSources.end(),
|
||||
[name](auto d) {
|
||||
return d->title == QString(name);
|
||||
}),
|
||||
m_audioSources.end());
|
||||
Q_EMIT audioSourceRemoved();
|
||||
}
|
||||
g_free(name);
|
||||
gst_object_unref(device);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QVector>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
struct AudioSource {
|
||||
QString title;
|
||||
GstDevice *device;
|
||||
bool isDefault;
|
||||
};
|
||||
struct VideoCap {
|
||||
int width;
|
||||
int height;
|
||||
QVector<float> framerates;
|
||||
};
|
||||
|
||||
struct VideoSource {
|
||||
QString title;
|
||||
GstDevice *device;
|
||||
QVector<VideoCap> caps;
|
||||
};
|
||||
|
||||
class DeviceMonitor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static DeviceMonitor &instance()
|
||||
{
|
||||
static DeviceMonitor _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
QVector<AudioSource *> audioSources() const;
|
||||
QVector<VideoSource *> videoSources() const;
|
||||
bool callback(GstMessage *message);
|
||||
void init();
|
||||
|
||||
Q_SIGNALS:
|
||||
void videoSourceAdded();
|
||||
void audioSourceAdded();
|
||||
|
||||
void videoSourceRemoved();
|
||||
void audioSourceRemoved();
|
||||
|
||||
private:
|
||||
DeviceMonitor();
|
||||
GstDeviceMonitor *m_monitor = nullptr;
|
||||
QVector<AudioSource *> m_audioSources;
|
||||
QVector<VideoSource *> m_videoSources;
|
||||
void handleVideoSource(GstDevice *device);
|
||||
void handleAudioSource(GstDevice *device);
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "videosources.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
// #include "pipelinemanager.h"
|
||||
#include <QDebug>
|
||||
#include <QString>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
#include "neochatconfig.h"
|
||||
|
||||
int VideoSources::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return DeviceMonitor::instance().videoSources().size();
|
||||
}
|
||||
|
||||
QVariant VideoSources::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (index.row() >= DeviceMonitor::instance().videoSources().size()) {
|
||||
return QVariant(QStringLiteral("DEADBEEF"));
|
||||
}
|
||||
if (role == TitleRole) {
|
||||
return DeviceMonitor::instance().videoSources()[index.row()]->title;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> VideoSources::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TitleRole, "title"},
|
||||
};
|
||||
}
|
||||
|
||||
VideoSources::VideoSources()
|
||||
: QAbstractListModel()
|
||||
{
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::videoSourceAdded, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
connect(&DeviceMonitor::instance(), &DeviceMonitor::videoSourceRemoved, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
Q_EMIT currentIndexChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void VideoSources::foo(int index)
|
||||
{
|
||||
auto device = DeviceMonitor::instance().videoSources()[index]->device;
|
||||
|
||||
auto bin = gst_bin_new(nullptr);
|
||||
|
||||
GstElement *videoconvert = gst_element_factory_make("videoconvert", nullptr);
|
||||
// GstElement *videorate = gst_element_factory_make("videorate", nullptr);
|
||||
|
||||
GstElement *filter = gst_element_factory_make("capsfilter", nullptr);
|
||||
GstCaps *caps = gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, 1920, "height", G_TYPE_INT, 1080, "framerate", GST_TYPE_FRACTION, 5, 1, nullptr);
|
||||
g_object_set(filter, "caps", caps, nullptr);
|
||||
gst_caps_unref(caps);
|
||||
GstElement *deviceElement = gst_device_create_element(device, nullptr);
|
||||
|
||||
gst_bin_add_many(GST_BIN(bin), deviceElement, videoconvert, filter, nullptr);
|
||||
gst_element_link_many(deviceElement, videoconvert, filter, nullptr);
|
||||
|
||||
// GstPad *pad = gst_element_get_static_pad(filter, "src");
|
||||
GstPad *pad = gst_element_get_static_pad(filter, "src");
|
||||
auto ghostpad = gst_ghost_pad_new("src", pad);
|
||||
gst_element_add_pad(bin, ghostpad);
|
||||
gst_object_unref(pad);
|
||||
// PipelineManager::instance().add(bin);
|
||||
}
|
||||
|
||||
const VideoSource *VideoSources::currentDevice() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->camera();
|
||||
for (const auto &videoSource : DeviceMonitor::instance().videoSources()) {
|
||||
if (videoSource->title == name) {
|
||||
qDebug() << "WebRTC: camera:" << name;
|
||||
return videoSource;
|
||||
}
|
||||
}
|
||||
if (DeviceMonitor::instance().videoSources().length() == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return DeviceMonitor::instance().videoSources()[0];
|
||||
}
|
||||
|
||||
void VideoSources::setCurrentIndex(int index)
|
||||
{
|
||||
if (DeviceMonitor::instance().videoSources().size() == 0) {
|
||||
return;
|
||||
}
|
||||
NeoChatConfig::setCamera(DeviceMonitor::instance().videoSources()[index]->title);
|
||||
NeoChatConfig::self()->save();
|
||||
|
||||
setCapsIndex(0);
|
||||
}
|
||||
|
||||
int VideoSources::currentIndex() const
|
||||
{
|
||||
const auto config = NeoChatConfig::self();
|
||||
const QString name = config->camera();
|
||||
for (auto i = 0; i < DeviceMonitor::instance().videoSources().size(); i++) {
|
||||
if (DeviceMonitor::instance().videoSources()[i]->title == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QStringList VideoSources::caps(int index) const
|
||||
{
|
||||
if (index >= DeviceMonitor::instance().videoSources().size()) {
|
||||
return QStringList();
|
||||
}
|
||||
const auto &caps = DeviceMonitor::instance().videoSources()[index]->caps;
|
||||
QStringList strings;
|
||||
for (const auto &cap : caps) {
|
||||
strings += QStringLiteral("%1x%2, %3 FPS").arg(cap.width).arg(cap.height).arg(cap.framerates.back());
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
void VideoSources::setCapsIndex(int index)
|
||||
{
|
||||
NeoChatConfig::self()->setCameraCaps(index);
|
||||
NeoChatConfig::self()->save();
|
||||
Q_EMIT capsIndexChanged();
|
||||
}
|
||||
|
||||
int VideoSources::capsIndex() const
|
||||
{
|
||||
return NeoChatConfig::self()->cameraCaps();
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractListModel>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
#include "devicemonitor.h"
|
||||
|
||||
class VideoSources : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
|
||||
Q_PROPERTY(int capsIndex READ capsIndex WRITE setCapsIndex NOTIFY capsIndexChanged)
|
||||
public:
|
||||
enum Roles {
|
||||
TitleRole = Qt::UserRole + 1,
|
||||
DeviceRole,
|
||||
};
|
||||
|
||||
static VideoSources &instance()
|
||||
{
|
||||
static VideoSources _instance;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void foo(int index);
|
||||
|
||||
const VideoSource *currentDevice() const;
|
||||
|
||||
void setCurrentIndex(int index);
|
||||
int currentIndex() const;
|
||||
|
||||
void setCapsIndex(int index);
|
||||
int capsIndex() const;
|
||||
|
||||
Q_INVOKABLE QStringList caps(int index) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentIndexChanged();
|
||||
void capsIndexChanged();
|
||||
|
||||
private:
|
||||
VideoSources();
|
||||
};
|
||||
177
src/chatbarcache.cpp
Normal file
177
src/chatbarcache.cpp
Normal file
@@ -0,0 +1,177 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "chatbarcache.h"
|
||||
|
||||
#include "eventhandler.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
ChatBarCache::ChatBarCache(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString ChatBarCache::text() const
|
||||
{
|
||||
return m_text;
|
||||
}
|
||||
|
||||
void ChatBarCache::setText(const QString &text)
|
||||
{
|
||||
if (text == m_text) {
|
||||
return;
|
||||
}
|
||||
m_text = text;
|
||||
Q_EMIT textChanged();
|
||||
}
|
||||
|
||||
bool ChatBarCache::isReplying() const
|
||||
{
|
||||
return m_relationType == Reply && !m_relationId.isEmpty();
|
||||
}
|
||||
|
||||
QString ChatBarCache::replyId() const
|
||||
{
|
||||
if (m_relationType != Reply) {
|
||||
return {};
|
||||
}
|
||||
return m_relationId;
|
||||
}
|
||||
|
||||
void ChatBarCache::setReplyId(const QString &replyId)
|
||||
{
|
||||
if (m_relationType == Reply && m_relationId == replyId) {
|
||||
return;
|
||||
}
|
||||
m_relationId = replyId;
|
||||
if (m_relationId.isEmpty()) {
|
||||
m_relationType = None;
|
||||
} else {
|
||||
m_relationType = Reply;
|
||||
}
|
||||
m_attachmentPath = QString();
|
||||
Q_EMIT relationIdChanged();
|
||||
Q_EMIT attachmentPathChanged();
|
||||
}
|
||||
|
||||
bool ChatBarCache::isEditing() const
|
||||
{
|
||||
return m_relationType == Edit && !m_relationId.isEmpty();
|
||||
}
|
||||
|
||||
QString ChatBarCache::editId() const
|
||||
{
|
||||
if (m_relationType != Edit) {
|
||||
return {};
|
||||
}
|
||||
return m_relationId;
|
||||
}
|
||||
|
||||
void ChatBarCache::setEditId(const QString &editId)
|
||||
{
|
||||
if (m_relationType == Edit && m_relationId == editId) {
|
||||
return;
|
||||
}
|
||||
m_relationId = editId;
|
||||
if (m_relationId.isEmpty()) {
|
||||
m_relationType = None;
|
||||
} else {
|
||||
m_relationType = Edit;
|
||||
}
|
||||
m_attachmentPath = QString();
|
||||
Q_EMIT relationIdChanged();
|
||||
Q_EMIT attachmentPathChanged();
|
||||
}
|
||||
|
||||
QVariantMap ChatBarCache::relationUser() const
|
||||
{
|
||||
if (parent() == nullptr) {
|
||||
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return {};
|
||||
}
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return {};
|
||||
}
|
||||
if (m_relationId.isEmpty()) {
|
||||
return room->getUser(nullptr);
|
||||
}
|
||||
return room->getUser(room->user((*room->findInTimeline(m_relationId))->senderId()));
|
||||
}
|
||||
|
||||
QString ChatBarCache::relationMessage() const
|
||||
{
|
||||
if (parent() == nullptr) {
|
||||
qWarning() << "ChatBarCache created with no parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return {};
|
||||
}
|
||||
if (m_relationId.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
auto room = dynamic_cast<NeoChatRoom *>(parent());
|
||||
if (room == nullptr) {
|
||||
qWarning() << "ChatBarCache created with incorrect parent, a NeoChatRoom must be set as the parent on creation.";
|
||||
return {};
|
||||
}
|
||||
EventHandler eventhandler;
|
||||
eventhandler.setRoom(room);
|
||||
if (auto event = room->findInTimeline(m_relationId); event != room->historyEdge()) {
|
||||
eventhandler.setEvent(&**event);
|
||||
return eventhandler.getPlainBody();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ChatBarCache::isThreaded() const
|
||||
{
|
||||
return !m_threadId.isEmpty();
|
||||
}
|
||||
|
||||
QString ChatBarCache::threadId() const
|
||||
{
|
||||
return m_threadId;
|
||||
}
|
||||
|
||||
void ChatBarCache::setThreadId(const QString &threadId)
|
||||
{
|
||||
if (m_threadId == threadId) {
|
||||
return;
|
||||
}
|
||||
m_threadId = threadId;
|
||||
Q_EMIT threadIdChanged();
|
||||
}
|
||||
|
||||
QString ChatBarCache::attachmentPath() const
|
||||
{
|
||||
return m_attachmentPath;
|
||||
}
|
||||
|
||||
void ChatBarCache::setAttachmentPath(const QString &attachmentPath)
|
||||
{
|
||||
if (attachmentPath == m_attachmentPath) {
|
||||
return;
|
||||
}
|
||||
m_attachmentPath = attachmentPath;
|
||||
m_relationType = None;
|
||||
m_relationId = QString();
|
||||
Q_EMIT attachmentPathChanged();
|
||||
Q_EMIT relationIdChanged();
|
||||
}
|
||||
|
||||
QList<Mention> *ChatBarCache::mentions()
|
||||
{
|
||||
return &m_mentions;
|
||||
}
|
||||
|
||||
QString ChatBarCache::savedText() const
|
||||
{
|
||||
return m_savedText;
|
||||
}
|
||||
|
||||
void ChatBarCache::setSavedText(const QString &savedText)
|
||||
{
|
||||
m_savedText = savedText;
|
||||
}
|
||||
|
||||
#include "moc_chatbarcache.cpp"
|
||||
201
src/chatbarcache.h
Normal file
201
src/chatbarcache.h
Normal file
@@ -0,0 +1,201 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QTextCursor>
|
||||
|
||||
/**
|
||||
* @brief Defines a user mention in the current chat or edit text.
|
||||
*/
|
||||
struct Mention {
|
||||
QTextCursor cursor; /**< Contains the mention's text and position in the text. */
|
||||
QString text; /**< The inserted text of the mention. */
|
||||
int start = 0; /**< Start position of the mention. */
|
||||
int position = 0; /**< End position of the mention. */
|
||||
QString id; /**< The id the mention (used to create link when sending the message). */
|
||||
};
|
||||
|
||||
/**
|
||||
* @class ChatBarCache
|
||||
*
|
||||
* A class to cache data from a chat bar.
|
||||
*
|
||||
* A chat bar can be anything that allows users to compose or edit message, it doesn't
|
||||
* necessarily have to use the ChatBar component, e.g. MessageEditComponent.
|
||||
*
|
||||
* This object is intended to allow the current contents of a chat bar to be cached
|
||||
* between different rooms, i.e. there is an expectation that each NeoChatRoom could
|
||||
* have a separate cache for each chat bar.
|
||||
*
|
||||
* @note The NeoChatRoom which this component is created in is expected to be set
|
||||
* as it's parent. This is necessary for certain functions which need to get
|
||||
* relevant room information.
|
||||
*
|
||||
* @sa ChatBar, MessageEditComponent, NeoChatRoom
|
||||
*/
|
||||
class ChatBarCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
/**
|
||||
* @brief The text in the chat bar.
|
||||
*
|
||||
* Due to problems with QTextDocument, unlike the other properties here,
|
||||
* text is *not* used to store the text when switching rooms.
|
||||
*/
|
||||
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the chat bar is currently replying to a message.
|
||||
*/
|
||||
Q_PROPERTY(bool isReplying READ isReplying NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief The Matrix message ID of an event being replied to, if any.
|
||||
*
|
||||
* Will return empty if the RelationType is currently set to None or Edit.
|
||||
*
|
||||
* @note Replying, editing and attachments are exclusive so setting this will
|
||||
* clear an edit or attachment.
|
||||
*
|
||||
* @sa RelationType
|
||||
*/
|
||||
Q_PROPERTY(QString replyId READ replyId WRITE setReplyId NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the chat bar is currently editing a message.
|
||||
*/
|
||||
Q_PROPERTY(bool isEditing READ isEditing NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief The Matrix message ID of an event being edited, if any.
|
||||
*
|
||||
* Will return empty if the RelationType is currently set to None or Reply.
|
||||
*
|
||||
* @note Replying, editing and attachments are exclusive so setting this will
|
||||
* clear an reply or attachment.
|
||||
*
|
||||
* @sa RelationType
|
||||
*/
|
||||
Q_PROPERTY(QString editId READ editId WRITE setEditId NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief Get the user for the message being replied to.
|
||||
*
|
||||
* This is different to getting a Quotient::User object
|
||||
* as neither of those can provide details like the displayName or avatarMediaId
|
||||
* without the room context as these can vary from room to room.
|
||||
*
|
||||
* Returns an empty user if not replying to a message.
|
||||
*
|
||||
* The user QVariantMap has the following properties:
|
||||
* - isLocalUser - Whether the user is the local user.
|
||||
* - id - The matrix ID of the user.
|
||||
* - displayName - Display name in the context of this room.
|
||||
* - avatarSource - The mxc URL for the user's avatar in the current room.
|
||||
* - avatarMediaId - Avatar id in the context of this room.
|
||||
* - color - Color for the user.
|
||||
* - object - The Quotient::User object for the user.
|
||||
*
|
||||
* @sa getUser, Quotient::User
|
||||
*/
|
||||
Q_PROPERTY(QVariantMap relationUser READ relationUser NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief The content of the related message.
|
||||
*
|
||||
* Will be QString() if no related message.
|
||||
*/
|
||||
Q_PROPERTY(QString relationMessage READ relationMessage NOTIFY relationIdChanged)
|
||||
|
||||
/**
|
||||
* @brief Whether the chat bar is replying in a thread.
|
||||
*/
|
||||
Q_PROPERTY(bool isThreaded READ isThreaded NOTIFY threadIdChanged)
|
||||
|
||||
/**
|
||||
* @brief The Matrix message ID of thread root event, if any.
|
||||
*/
|
||||
Q_PROPERTY(QString threadId READ threadId WRITE setThreadId NOTIFY threadIdChanged)
|
||||
|
||||
/**
|
||||
* @brief The local path for a file to send, if any.
|
||||
*
|
||||
* @note Replying, editing and attachments are exclusive so setting this will
|
||||
* clear an edit or reply.
|
||||
*/
|
||||
Q_PROPERTY(QString attachmentPath READ attachmentPath WRITE setAttachmentPath NOTIFY attachmentPathChanged)
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Describes the type of relation which relationId can refer to.
|
||||
*
|
||||
* A chat bar can only be relating to a single message at a time making these
|
||||
* exclusive.
|
||||
*/
|
||||
enum RelationType {
|
||||
Reply, /**< The current relation is a message being replied to. */
|
||||
Edit, /**< The current relation is a message being edited. */
|
||||
None, /**< There is currently no relation event */
|
||||
};
|
||||
Q_ENUM(RelationType)
|
||||
|
||||
explicit ChatBarCache(QObject *parent = nullptr);
|
||||
|
||||
QString text() const;
|
||||
void setText(const QString &text);
|
||||
|
||||
bool isReplying() const;
|
||||
QString replyId() const;
|
||||
void setReplyId(const QString &replyId);
|
||||
|
||||
bool isEditing() const;
|
||||
QString editId() const;
|
||||
void setEditId(const QString &editId);
|
||||
|
||||
QVariantMap relationUser() const;
|
||||
|
||||
QString relationMessage() const;
|
||||
|
||||
bool isThreaded() const;
|
||||
QString threadId() const;
|
||||
void setThreadId(const QString &threadId);
|
||||
|
||||
QString attachmentPath() const;
|
||||
void setAttachmentPath(const QString &attachmentPath);
|
||||
|
||||
/**
|
||||
* @brief Retrieve the mentions for the current chat bar text.
|
||||
*/
|
||||
QList<Mention> *mentions();
|
||||
|
||||
/**
|
||||
* @brief Get the saved chat bar text.
|
||||
*/
|
||||
QString savedText() const;
|
||||
|
||||
/**
|
||||
* @brief Save the chat bar text.
|
||||
*/
|
||||
void setSavedText(const QString &savedText);
|
||||
|
||||
Q_SIGNALS:
|
||||
void textChanged();
|
||||
void relationIdChanged();
|
||||
void threadIdChanged();
|
||||
void attachmentPathChanged();
|
||||
|
||||
private:
|
||||
QString m_text = QString();
|
||||
QString m_relationId = QString();
|
||||
RelationType m_relationType = RelationType::None;
|
||||
QString m_threadId = QString();
|
||||
QString m_attachmentPath = QString();
|
||||
QList<Mention> m_mentions;
|
||||
QString m_savedText;
|
||||
};
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <Sonnet/BackgroundChecker>
|
||||
#include <Sonnet/Settings>
|
||||
|
||||
#include "neochatroom.h"
|
||||
#include "chatdocumenthandler_logging.h"
|
||||
|
||||
class SyntaxHighlighter : public QSyntaxHighlighter
|
||||
{
|
||||
@@ -59,11 +59,12 @@ public:
|
||||
setFormat(error.first, error.second.size(), errorFormat);
|
||||
}
|
||||
}
|
||||
auto room = dynamic_cast<ChatDocumentHandler *>(parent())->room();
|
||||
auto handler = dynamic_cast<ChatDocumentHandler *>(parent());
|
||||
auto room = handler->room();
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
auto mentions = room->mentions();
|
||||
auto mentions = handler->chatBarCache()->mentions();
|
||||
mentions->erase(std::remove_if(mentions->begin(),
|
||||
mentions->end(),
|
||||
[this](auto &mention) {
|
||||
@@ -99,21 +100,16 @@ ChatDocumentHandler::ChatDocumentHandler(QObject *parent)
|
||||
, m_document(nullptr)
|
||||
, m_cursorPosition(-1)
|
||||
, m_highlighter(new SyntaxHighlighter(this))
|
||||
, m_completionModel(new CompletionModel())
|
||||
, m_completionModel(new CompletionModel(this))
|
||||
{
|
||||
connect(this, &ChatDocumentHandler::roomChanged, this, [this]() {
|
||||
m_completionModel->setRoom(m_room);
|
||||
static QPointer<NeoChatRoom> previousRoom = nullptr;
|
||||
if (previousRoom) {
|
||||
disconnect(previousRoom, &NeoChatRoom::chatBoxTextChanged, this, nullptr);
|
||||
disconnect(previousRoom, &NeoChatRoom::editTextChanged, this, nullptr);
|
||||
disconnect(m_chatBarCache, &ChatBarCache::textChanged, this, nullptr);
|
||||
}
|
||||
previousRoom = m_room;
|
||||
connect(m_room, &NeoChatRoom::chatBoxTextChanged, this, [this]() {
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
connect(m_room, &NeoChatRoom::editTextChanged, this, [this]() {
|
||||
connect(m_chatBarCache, &ChatBarCache::textChanged, this, [this]() {
|
||||
int start = completionStartIndex();
|
||||
m_completionModel->setText(getText().mid(start, cursorPosition() - start), getText().mid(start));
|
||||
});
|
||||
@@ -136,11 +132,7 @@ int ChatDocumentHandler::completionStartIndex() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
|
||||
const long long cursor = cursorPosition();
|
||||
#else
|
||||
const auto cursor = cursorPosition();
|
||||
#endif
|
||||
const qsizetype cursor = cursorPosition();
|
||||
const auto &text = getText();
|
||||
|
||||
auto start = std::min(cursor, text.size()) - 1;
|
||||
@@ -154,20 +146,6 @@ int ChatDocumentHandler::completionStartIndex() const
|
||||
return start;
|
||||
}
|
||||
|
||||
bool ChatDocumentHandler::isEdit() const
|
||||
{
|
||||
return m_isEdit;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setIsEdit(bool edit)
|
||||
{
|
||||
if (edit == m_isEdit) {
|
||||
return;
|
||||
}
|
||||
m_isEdit = edit;
|
||||
Q_EMIT isEditChanged();
|
||||
}
|
||||
|
||||
QQuickTextDocument *ChatDocumentHandler::document() const
|
||||
{
|
||||
return m_document;
|
||||
@@ -217,24 +195,47 @@ void ChatDocumentHandler::setRoom(NeoChatRoom *room)
|
||||
Q_EMIT roomChanged();
|
||||
}
|
||||
|
||||
ChatBarCache *ChatDocumentHandler::chatBarCache() const
|
||||
{
|
||||
return m_chatBarCache;
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::setChatBarCache(ChatBarCache *chatBarCache)
|
||||
{
|
||||
if (m_chatBarCache == chatBarCache) {
|
||||
return;
|
||||
}
|
||||
m_chatBarCache = chatBarCache;
|
||||
Q_EMIT chatBarCacheChanged();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::complete(int index)
|
||||
{
|
||||
if (m_document == nullptr) {
|
||||
qCWarning(ChatDocumentHandling) << "complete called with m_document set to nullptr.";
|
||||
return;
|
||||
}
|
||||
if (m_completionModel->autoCompletionType() == CompletionModel::None) {
|
||||
qCWarning(ChatDocumentHandling) << "complete called with m_completionModel->autoCompletionType() == CompletionModel::None.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_completionModel->autoCompletionType() == CompletionModel::User) {
|
||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Text).toString();
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||
auto name = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::DisplayNameRole).toString();
|
||||
auto id = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('@'), cursorPosition() - 1);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(name % " ");
|
||||
cursor.insertText(name + QStringLiteral(" "));
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + name.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, name, 0, 0, id});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Command) {
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||
auto command = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('/'));
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
@@ -242,20 +243,20 @@ void ChatDocumentHandler::complete(int index)
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(QStringLiteral("/%1 ").arg(command));
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Room) {
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::Subtitle).toString();
|
||||
auto alias = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::SubtitleRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char('#'), cursorPosition() - 1);
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursorPosition(), QTextCursor::KeepAnchor);
|
||||
cursor.insertText(alias % " ");
|
||||
cursor.insertText(alias + QStringLiteral(" "));
|
||||
cursor.setPosition(at);
|
||||
cursor.setPosition(cursor.position() + alias.size(), QTextCursor::KeepAnchor);
|
||||
cursor.setKeepPositionOnInsert(true);
|
||||
pushMention({cursor, alias, 0, 0, alias});
|
||||
m_highlighter->rehighlight();
|
||||
} else if (m_completionModel->autoCompletionType() == CompletionModel::Emoji) {
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedText).toString();
|
||||
auto shortcode = m_completionModel->data(m_completionModel->index(index, 0), CompletionModel::ReplacedTextRole).toString();
|
||||
auto text = getText();
|
||||
auto at = text.lastIndexOf(QLatin1Char(':'));
|
||||
QTextCursor cursor(document()->textDocument());
|
||||
@@ -302,26 +303,20 @@ void ChatDocumentHandler::setSelectionEnd(int position)
|
||||
|
||||
QString ChatDocumentHandler::getText() const
|
||||
{
|
||||
if (!m_room) {
|
||||
return QString();
|
||||
}
|
||||
if (m_isEdit) {
|
||||
return m_room->editText();
|
||||
} else {
|
||||
return m_room->chatBoxText();
|
||||
if (!m_chatBarCache) {
|
||||
qCWarning(ChatDocumentHandling) << "getText called with m_chatBarCache set to nullptr.";
|
||||
return {};
|
||||
}
|
||||
return m_chatBarCache->text();
|
||||
}
|
||||
|
||||
void ChatDocumentHandler::pushMention(const Mention mention) const
|
||||
{
|
||||
if (!m_room) {
|
||||
if (!m_chatBarCache) {
|
||||
qCWarning(ChatDocumentHandling) << "pushMention called with m_chatBarCache set to nullptr.";
|
||||
return;
|
||||
}
|
||||
if (m_isEdit) {
|
||||
m_room->editMentions()->push_back(mention);
|
||||
} else {
|
||||
m_room->mentions()->push_back(mention);
|
||||
}
|
||||
m_chatBarCache->mentions()->push_back(mention);
|
||||
}
|
||||
|
||||
QColor ChatDocumentHandler::mentionColor() const
|
||||
@@ -355,3 +350,5 @@ void ChatDocumentHandler::setErrorColor(const QColor &color)
|
||||
m_highlighter->rehighlight();
|
||||
Q_EMIT errorColorChanged();
|
||||
}
|
||||
|
||||
#include "moc_chatdocumenthandler.cpp"
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "models/completionmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
class QTextDocument;
|
||||
class NeoChatRoom;
|
||||
class SyntaxHighlighter;
|
||||
|
||||
@@ -59,14 +60,7 @@ class SyntaxHighlighter;
|
||||
class ChatDocumentHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* @brief Is the instance being used to handle an edit message.
|
||||
*
|
||||
* This is needed to ensure that the text and mentions are saved and retrieved
|
||||
* from the correct parameters in the assigned room.
|
||||
*/
|
||||
Q_PROPERTY(bool isEdit READ isEdit WRITE setIsEdit NOTIFY isEditChanged)
|
||||
QML_ELEMENT
|
||||
|
||||
/**
|
||||
* @brief The QQuickTextDocument that is being handled.
|
||||
@@ -94,29 +88,31 @@ class ChatDocumentHandler : public QObject
|
||||
* This is typically provided to a qml component to visualise the current
|
||||
* completion results.
|
||||
*/
|
||||
Q_PROPERTY(CompletionModel *completionModel READ completionModel NOTIFY completionModelChanged)
|
||||
Q_PROPERTY(CompletionModel *completionModel READ completionModel CONSTANT)
|
||||
|
||||
/**
|
||||
* @brief The current room that the the text document is being handled for.
|
||||
*/
|
||||
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
|
||||
|
||||
/**
|
||||
* @brief The cache for the chat bar the text document is being handled for.
|
||||
*/
|
||||
Q_PROPERTY(ChatBarCache *chatBarCache READ chatBarCache WRITE setChatBarCache NOTIFY chatBarCacheChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight user mentions.
|
||||
*/
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged);
|
||||
Q_PROPERTY(QColor mentionColor READ mentionColor WRITE setMentionColor NOTIFY mentionColorChanged)
|
||||
|
||||
/**
|
||||
* @brief The color to highlight spelling errors.
|
||||
*/
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged);
|
||||
Q_PROPERTY(QColor errorColor READ errorColor WRITE setErrorColor NOTIFY errorColorChanged)
|
||||
|
||||
public:
|
||||
explicit ChatDocumentHandler(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] bool isEdit() const;
|
||||
void setIsEdit(bool edit);
|
||||
|
||||
[[nodiscard]] QQuickTextDocument *document() const;
|
||||
void setDocument(QQuickTextDocument *document);
|
||||
|
||||
@@ -132,9 +128,11 @@ public:
|
||||
[[nodiscard]] NeoChatRoom *room() const;
|
||||
void setRoom(NeoChatRoom *room);
|
||||
|
||||
[[nodiscard]] ChatBarCache *chatBarCache() const;
|
||||
void setChatBarCache(ChatBarCache *chatBarCache);
|
||||
|
||||
Q_INVOKABLE void complete(int index);
|
||||
|
||||
void updateCompletions();
|
||||
CompletionModel *completionModel() const;
|
||||
|
||||
[[nodiscard]] QColor mentionColor() const;
|
||||
@@ -144,11 +142,10 @@ public:
|
||||
void setErrorColor(const QColor &color);
|
||||
|
||||
Q_SIGNALS:
|
||||
void isEditChanged();
|
||||
void documentChanged();
|
||||
void cursorPositionChanged();
|
||||
void roomChanged();
|
||||
void completionModelChanged();
|
||||
void chatBarCacheChanged();
|
||||
void selectionStartChanged();
|
||||
void selectionEndChanged();
|
||||
void errorColorChanged();
|
||||
@@ -157,12 +154,10 @@ Q_SIGNALS:
|
||||
private:
|
||||
int completionStartIndex() const;
|
||||
|
||||
bool m_isEdit = false;
|
||||
|
||||
QPointer<QQuickTextDocument> m_document;
|
||||
|
||||
QPointer<NeoChatRoom> m_room;
|
||||
bool completionVisible = false;
|
||||
QPointer<ChatBarCache> m_chatBarCache;
|
||||
|
||||
QColor m_mentionColor;
|
||||
QColor m_errorColor;
|
||||
@@ -176,7 +171,5 @@ private:
|
||||
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
|
||||
CompletionModel::AutoCompletionType m_completionType = CompletionModel::None;
|
||||
|
||||
CompletionModel *m_completionModel = nullptr;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user