Compare commits
589 Commits
Author | SHA1 | Date |
---|---|---|
Nicolas Werner | 1f68bc60b5 | 4 years ago |
Nicolas Werner | c25fd947a8 | 4 years ago |
Nicolas Werner | bd0deb7213 | 4 years ago |
Nicolas Werner | bc7494473b | 4 years ago |
DeepBlueV7.X | b23913fa7c | 4 years ago |
kamathmanu | e1c4f7d516 | 4 years ago |
kamathmanu | e3c6656613 | 4 years ago |
Weblate | 70b5e1e08e | 4 years ago |
kamathmanu | a0c2a174ea | 4 years ago |
Nicolas Werner | 62d0bdbb77 | 4 years ago |
Nicolas Werner | d075a90024 | 4 years ago |
Nicolas Werner | 716992b761 | 4 years ago |
Joseph Donofry | 3499abd99a | 4 years ago |
Joseph Donofry | 31c6857f19 | 4 years ago |
DeepBlueV7.X | 82374791c4 | 4 years ago |
DeepBlueV7.X | a9c0684a5a | 4 years ago |
Nicolas Werner | 1a029112d9 | 4 years ago |
DeepBlueV7.X | 517a126a44 | 4 years ago |
Nicolas Werner | 392d7d5568 | 4 years ago |
Nicolas Werner | 57a6c05eab | 4 years ago |
Nicolas Werner | 684cfacfad | 4 years ago |
Nicolas Werner | 56ba7de501 | 4 years ago |
Nicolas Werner | 8ec76daeda | 4 years ago |
Nicolas Werner | e5fb9a25ea | 4 years ago |
Nicolas Werner | 08d5a84cbd | 4 years ago |
Nicolas Werner | 99ba1f17d3 | 4 years ago |
Nicolas Werner | cd43147b77 | 4 years ago |
Nicolas Werner | 7b6fab3373 | 4 years ago |
LorenDB | d7c3fa844e | 4 years ago |
Nicolas Werner | 64d5a193f1 | 4 years ago |
Nicolas Werner | 8a4d85f801 | 4 years ago |
Nicolas Werner | bca29a4227 | 4 years ago |
Nicolas Werner | 2a79cd2b6b | 4 years ago |
Nicolas Werner | abff61bb6c | 4 years ago |
Nicolas Werner | 51964c4fd7 | 4 years ago |
Nicolas Werner | d4dccc8e36 | 4 years ago |
Nicolas Werner | 600df6d2ec | 4 years ago |
Nicolas Werner | c9de044e32 | 4 years ago |
Nicolas Werner | 54e3c2c96b | 4 years ago |
DeepBlueV7.X | 8110f22222 | 4 years ago |
Nicolas Werner | 94690ebd4c | 4 years ago |
trilene | 28e9a7ad40 | 4 years ago |
trilene | 4caa206483 | 4 years ago |
Weblate | 1a97859930 | 4 years ago |
DeepBlueV7.X | e5cb8c08ba | 4 years ago |
trilene | 5bfe0cd178 | 4 years ago |
DeepBlueV7.X | 5cce5b9999 | 4 years ago |
trilene | e57199412a | 4 years ago |
trilene | aab6cb88a5 | 4 years ago |
trilene | 3f73853e4b | 4 years ago |
Nicolas Werner | 4802c34009 | 4 years ago |
Weblate | 5e0eb945ae | 4 years ago |
Weblate | 0cca0f17ee | 4 years ago |
trilene | 44cfc8d22a | 4 years ago |
trilene | da27670cbe | 4 years ago |
Weblate | 9169a26e67 | 4 years ago |
Nicolas Werner | 54db9c89ed | 4 years ago |
Nicolas Werner | 67302b3674 | 4 years ago |
Nicolas Werner | 4179be427f | 4 years ago |
Weblate | beef474582 | 4 years ago |
Weblate | e59bf3564d | 4 years ago |
Nicolas Werner | 0afe1299e2 | 4 years ago |
Nicolas Werner | 7b7d29737f | 4 years ago |
DeepBlueV7.X | fe325f9266 | 4 years ago |
Weblate | 6c63762aa4 | 4 years ago |
Weblate | 0a7189b6a1 | 4 years ago |
Weblate | b4eb1c6df9 | 4 years ago |
Weblate | a253eda5fd | 4 years ago |
Weblate | 145e207848 | 4 years ago |
Weblate | 78572d9d88 | 4 years ago |
Weblate | b2c3639f04 | 4 years ago |
Nicolas Werner | 4b36585e47 | 4 years ago |
Weblate | 1e0962d97c | 4 years ago |
Nicolas Werner | 75d7c2608a | 4 years ago |
Weblate | f10970cc83 | 4 years ago |
Weblate | ea2f7283ca | 4 years ago |
Weblate | 8cb45ef834 | 4 years ago |
Weblate | 5c5134cea2 | 4 years ago |
Jussi Kuokkanen | 13658d536e | 4 years ago |
Jussi Kuokkanen | bafe181010 | 4 years ago |
Jussi Kuokkanen | 438dcd3c5e | 4 years ago |
Lurkki14 | 8741e5f36a | 4 years ago |
DeepBlueV7.X | 791a01487b | 4 years ago |
trilene | e065bf2205 | 4 years ago |
trilene | f7beb1b34e | 4 years ago |
trilene | 8d7c70d912 | 4 years ago |
trilene | b527c5a21c | 4 years ago |
Jussi Kuokkanen | 1f71f7227a | 4 years ago |
Jussi Kuokkanen | c463568031 | 4 years ago |
Jussi Kuokkanen | 7d72356318 | 4 years ago |
Jussi Kuokkanen | e49691fe3b | 4 years ago |
DeepBlueV7.X | c62db00e6f | 4 years ago |
Nicolas Werner | c5f93efcd3 | 4 years ago |
Nicolas Werner | 8af056faa9 | 4 years ago |
Nicolas Werner | fef0cc2d71 | 4 years ago |
Nicolas Werner | c2f2e8324c | 4 years ago |
Nicolas Werner | b05c101021 | 4 years ago |
Nicolas Werner | 77e241b9e5 | 4 years ago |
Nicolas Werner | 640b0ee405 | 4 years ago |
DeepBlueV7.X | 66d6307252 | 4 years ago |
trilene | 2526a5604e | 4 years ago |
Weblate | 9b8e696979 | 4 years ago |
Weblate | 83f0e2772c | 4 years ago |
Weblate | 25b5a21ecd | 4 years ago |
Weblate | be2c4e5021 | 4 years ago |
Weblate | ff8cec1ea3 | 4 years ago |
DeepBlueV7.X | d02e899941 | 4 years ago |
trilene | bd0e66b548 | 4 years ago |
CH Chethan Reddy | 8eb74daf76 | 4 years ago |
Weblate | b5669310e5 | 4 years ago |
Weblate | a09a37cc3c | 4 years ago |
Nicolas Werner | 94e1b52ddd | 4 years ago |
Nicolas Werner | b934cf329c | 4 years ago |
DeepBlueV7.X | 75e646968d | 4 years ago |
DeepBlueV7.X | a7b979084f | 4 years ago |
trilene | 124952a11c | 4 years ago |
DeepBlueV7.X | d38a4dcf5d | 4 years ago |
Jussi Kuokkanen | bfcfa79d53 | 4 years ago |
Chethan2k1 | f6a47ce72f | 4 years ago |
DeepBlueV7.X | b37534aa53 | 4 years ago |
trilene | b6563d9ffe | 4 years ago |
trilene | 7d2844b2b0 | 4 years ago |
Chethan2k1 | 2b5deabbdc | 4 years ago |
Chethan2k1 | a27662dc08 | 4 years ago |
Chethan2k1 | e70b4e4268 | 4 years ago |
Chethan2k1 | 898be090af | 4 years ago |
Nicolas Werner | c4e4938d35 | 4 years ago |
Chethan | 0b03d40bf5 | 4 years ago |
Chethan2k1 | db0d10f38e | 4 years ago |
Chethan2k1 | e8eeb480d5 | 4 years ago |
DeepBlueV7.X | 657f4073e9 | 4 years ago |
CH Chethan Reddy | 5358854de3 | 4 years ago |
Nicolas Werner | 10f09d4f43 | 4 years ago |
Nicolas Werner | e0981e17a1 | 4 years ago |
Nicolas Werner | 8d14a058c6 | 4 years ago |
DeepBlueV7.X | eb554e8266 | 4 years ago |
Jussi Kuokkanen | 8f872f1961 | 4 years ago |
Jussi Kuokkanen | bb4636885d | 4 years ago |
Jussi Kuokkanen | beec2607fc | 4 years ago |
Jussi Kuokkanen | 254b7549eb | 4 years ago |
CH Chethan Reddy | f03a48eec5 | 4 years ago |
CH Chethan Reddy | b174bd9380 | 4 years ago |
CH Chethan Reddy | 3396a7a796 | 4 years ago |
CH Chethan Reddy | 9a76db85d5 | 4 years ago |
Jussi Kuokkanen | aed8d23aca | 4 years ago |
CH Chethan Reddy | 0d1dd29b19 | 4 years ago |
Jussi Kuokkanen | f40d8d15b5 | 4 years ago |
Jussi Kuokkanen | 9ad9c8ddf0 | 4 years ago |
Jussi Kuokkanen | 7acd4b3307 | 4 years ago |
Jussi Kuokkanen | 5e344d2685 | 4 years ago |
Jussi Kuokkanen | a173d964f7 | 4 years ago |
DeepBlueV7.X | b7b9cee30e | 4 years ago |
trilene | 67a6ab401b | 4 years ago |
DeepBlueV7.X | b58e370c03 | 4 years ago |
Nicolas Werner | 3df4bde032 | 4 years ago |
Nicolas Werner | 9f79b85579 | 4 years ago |
CH Chethan Reddy | 19cfd08a55 | 4 years ago |
CH Chethan Reddy | 1d299951b6 | 4 years ago |
DeepBlueV7.X | 898297a7b0 | 4 years ago |
trilene | 473293b6a5 | 4 years ago |
DeepBlueV7.X | 706ba84d73 | 4 years ago |
Tony O | d61d108a4f | 4 years ago |
CH Chethan Reddy | 8a4bd37fea | 4 years ago |
DeepBlueV7.X | b5d406ad80 | 4 years ago |
Nicolas Werner | d6bc05fcd6 | 4 years ago |
trilene | 1402732b5f | 4 years ago |
Nicolas Werner | 7f7108161e | 4 years ago |
Nicolas Werner | de7ec4d2b3 | 4 years ago |
Mihai Fufezan | 7c1ca38d98 | 4 years ago |
Mihai Fufezan | ed17b0c33b | 4 years ago |
DeepBlueV7.X | 3fece53eb7 | 4 years ago |
Nicolas Werner | b6751ab01e | 4 years ago |
Nicolas Werner | e06ff1ac1f | 4 years ago |
Nicolas Werner | f157602a52 | 4 years ago |
Nicolas Werner | 29cb065102 | 4 years ago |
Nicolas Werner | 14a0aac748 | 4 years ago |
CH Chethan Reddy | 2e20049b36 | 4 years ago |
Nicolas Werner | 1e9efa3072 | 4 years ago |
Nicolas Werner | b972d827cb | 4 years ago |
Nicolas Werner | 7eb0c4e09c | 4 years ago |
Nicolas Werner | 1f9215a5be | 4 years ago |
Nicolas Werner | dbaddb0165 | 4 years ago |
trilene | df65093374 | 4 years ago |
trilene | 02dfc8039f | 4 years ago |
trilene | b86711a388 | 4 years ago |
trilene | 979bba6460 | 4 years ago |
trilene | e527da052b | 4 years ago |
trilene | e3e7595bab | 4 years ago |
trilene | f14d141cb5 | 4 years ago |
trilene | c0743f9688 | 4 years ago |
HelaBasa | 48877307ca | 4 years ago |
CH Chethan Reddy | 3635c185e9 | 4 years ago |
CH Chethan Reddy | 1fcd768f88 | 4 years ago |
CH Chethan Reddy | a2979c2df1 | 4 years ago |
CH Chethan Reddy | 1103cc15cf | 4 years ago |
Nicolas Werner | 08028d5c57 | 4 years ago |
CH Chethan Reddy | ac1fbbb69f | 4 years ago |
CH Chethan Reddy | 6fae36abc4 | 4 years ago |
CH Chethan Reddy | ffa61095b8 | 4 years ago |
CH Chethan Reddy | 00e36b6068 | 4 years ago |
CH Chethan Reddy | 75efa5d3a2 | 4 years ago |
CH Chethan Reddy | fd232b1f4a | 4 years ago |
Nicolas Werner | 4862be06be | 4 years ago |
CH Chethan Reddy | 1633650303 | 4 years ago |
CH Chethan Reddy | d49ab15656 | 4 years ago |
CH Chethan Reddy | ce013e67a6 | 4 years ago |
CH Chethan Reddy | 67367d0004 | 4 years ago |
CH Chethan Reddy | 41b6ef0c32 | 4 years ago |
Chethan2k1 | 1eb162cb6f | 4 years ago |
Chethan2k1 | cd5dd0e39b | 4 years ago |
Chethan2k1 | b628f485ff | 4 years ago |
CH Chethan Reddy | f9c0f4dd54 | 4 years ago |
Chethan2k1 | a54a973ad6 | 4 years ago |
CH Chethan Reddy | 64f204d984 | 4 years ago |
Nicolas Werner | 707248fea3 | 4 years ago |
Nicolas Werner | b1362ca69f | 4 years ago |
Nicolas Werner | fed0463e57 | 4 years ago |
Nicolas Werner | 480c4bc8f5 | 4 years ago |
Nicolas Werner | 2088053d26 | 4 years ago |
Nicolas Werner | 488cc5e73b | 4 years ago |
Nicolas Werner | 7f3d97517f | 4 years ago |
trilene | 97681ccf64 | 4 years ago |
Nicolas Werner | 12090c0a06 | 4 years ago |
Nicolas Werner | 720bb164f7 | 4 years ago |
trilene | 43ec0c0624 | 4 years ago |
Nicolas Werner | ade905c881 | 4 years ago |
Nicolas Werner | a00b11def7 | 4 years ago |
Nicolas Werner | 28e7ea40cb | 4 years ago |
Nicolas Werner | 8bf26917ad | 4 years ago |
trilene | 57d5a3d31f | 4 years ago |
Nicolas Werner | 6f557c19a1 | 4 years ago |
Nicolas Werner | cbb4356b19 | 4 years ago |
Nicolas Werner | fdcf91f5eb | 4 years ago |
Nicolas Werner | 4e7bd20e0c | 4 years ago |
Nicolas Werner | b294430fe5 | 4 years ago |
trilene | 6be21beebd | 4 years ago |
trilene | aec24efbe2 | 4 years ago |
trilene | d51e34e66a | 4 years ago |
trilene | a4301048e3 | 4 years ago |
trilene | d508e3abd6 | 4 years ago |
trilene | 7377215d28 | 4 years ago |
trilene | 55783c6fe5 | 4 years ago |
Nicolas Werner | 147ae68c31 | 4 years ago |
trilene | 88cfa3a8fa | 4 years ago |
Nicolas Werner | f23d733cff | 4 years ago |
trilene | da9995fc3d | 4 years ago |
Nicolas Werner | 19f27236ea | 4 years ago |
DeepBlueV7.X | 12cb77da9c | 4 years ago |
Nicolas Werner | 6f2bc908ba | 4 years ago |
Lorem | e86c1cc79f | 4 years ago |
Nicolas Werner | d467568a65 | 4 years ago |
Nicolas Werner | 8261446f83 | 4 years ago |
Nicolas Werner | 5695f004a2 | 4 years ago |
Nicolas Werner | 046b3f4da6 | 4 years ago |
Nicolas Werner | 36e4405f25 | 4 years ago |
Nicolas Werner | a5dda86a6c | 4 years ago |
Nicolas Werner | 56ea89aa11 | 4 years ago |
trilene | 28a678ca60 | 4 years ago |
trilene | f8ef55c133 | 4 years ago |
Nicolas Werner | aa34576dfd | 4 years ago |
trilene | 8968d51b65 | 4 years ago |
trilene | 16209ce073 | 4 years ago |
trilene | 195ba5e5ee | 4 years ago |
trilene | 9d6cce9fe3 | 4 years ago |
trilene | 774d864096 | 4 years ago |
trilene | 09d2d937c5 | 4 years ago |
trilene | e85652e7e7 | 4 years ago |
Nicolas Werner | 9ae7d0dce3 | 4 years ago |
Nicolas Werner | 9479fcde08 | 4 years ago |
trilene | c73cfe1810 | 4 years ago |
trilene | 7a206441c8 | 4 years ago |
Nicolas Werner | da2f80df60 | 4 years ago |
Nicolas Werner | 3421728898 | 4 years ago |
Nicolas Werner | 530c531c4b | 4 years ago |
Nicolas Werner | 7650e6ced6 | 4 years ago |
Nicolas Werner | 8d3ab300b6 | 4 years ago |
Nicolas Werner | fe12e63c7c | 4 years ago |
Nicolas Werner | 0da1a6d5fc | 4 years ago |
Nicolas Werner | 82eff09062 | 4 years ago |
Nicolas Werner | c79205c26a | 4 years ago |
Nicolas Werner | 79a29953dd | 4 years ago |
Nicolas Werner | 233b3c06ce | 4 years ago |
Nicolas Werner | d72eb5eb2d | 4 years ago |
Nicolas Werner | 21a1f249f9 | 4 years ago |
Nicolas Werner | d177405913 | 4 years ago |
Nicolas Werner | f6fa494666 | 4 years ago |
Nicolas Werner | da975038db | 4 years ago |
Nicolas Werner | a9e321e38f | 4 years ago |
DeepBlueV7.X | 09e9cffcae | 4 years ago |
Felix Yan | 751c0526d7 | 4 years ago |
Nicolas Werner | c973fd759b | 4 years ago |
Nicolas Werner | cded494cb5 | 4 years ago |
Nicolas Werner | dde28c627b | 5 years ago |
Nicolas Werner | 5c8ee99e9a | 5 years ago |
DeepBlueV7.X | 887ad25ab7 | 5 years ago |
Lorem | e1706f4df5 | 5 years ago |
Nicolas Werner | 24d2a2d821 | 5 years ago |
Joseph Donofry | 95d5e63f4b | 5 years ago |
Joseph Donofry | 3ea5a92b2a | 5 years ago |
Nicolas Werner | a09039a3be | 5 years ago |
Joseph Donofry | ef51b5e947 | 5 years ago |
Joseph Donofry | 21dfb3c0b9 | 5 years ago |
Joseph Donofry | a96e6e5ecd | 5 years ago |
Joseph Donofry | 7afb164244 | 5 years ago |
Joseph Donofry | 813884ee0c | 5 years ago |
Joseph Donofry | dbaf92734c | 5 years ago |
Joseph Donofry | 34ed487c08 | 5 years ago |
Joseph Donofry | b90b718961 | 5 years ago |
Joseph Donofry | b9626f0c69 | 5 years ago |
Nicolas Werner | 866f59f79c | 5 years ago |
Nicolas Werner | 150c9b1dbc | 5 years ago |
Nicolas Werner | db93e6b853 | 5 years ago |
Nicolas Werner | 488924c9b3 | 5 years ago |
Joseph Donofry | 12c46e86b4 | 5 years ago |
Joseph Donofry | 5e355c36fd | 5 years ago |
DeepBlueV7.X | 75bb037bb7 | 5 years ago |
Alexander 'z33ky' Hirsch | 2b9860c3af | 5 years ago |
Joseph Donofry | 39b240e25a | 5 years ago |
Joseph Donofry | 73f4c26dbe | 5 years ago |
Joseph Donofry | 0078c72a37 | 5 years ago |
Joseph Donofry | 75cdc1eee2 | 5 years ago |
Joseph Donofry | 07ffd9e7e9 | 5 years ago |
Joseph Donofry | 6bb73f84a3 | 5 years ago |
Joseph Donofry | fa34749279 | 5 years ago |
Nicolas Werner | 0a23615dd7 | 5 years ago |
Nicolas Werner | b9631753dd | 5 years ago |
Nicolas Werner | 3baf11b5c4 | 5 years ago |
Nicolas Werner | 814868024c | 5 years ago |
Nicolas Werner | bf440f9a31 | 5 years ago |
Nicolas Werner | f2bfa61e08 | 5 years ago |
Nicolas Werner | 96f4169be9 | 5 years ago |
Nicolas Werner | e5a55ab1b9 | 5 years ago |
Nicolas Werner | 5e684a0a2f | 5 years ago |
Nicolas Werner | c40429af23 | 5 years ago |
Nicolas Werner | f4b84327e8 | 5 years ago |
Nicolas Werner | 43d2ebc095 | 5 years ago |
Nicolas Werner | f8903f493f | 5 years ago |
Nicolas Werner | 95f29a3d19 | 5 years ago |
Nicolas Werner | 5ca5b4561e | 5 years ago |
Nicolas Werner | 190b6cb3c7 | 5 years ago |
Nicolas Werner | c8ba385cb9 | 5 years ago |
Nicolas Werner | 50d5891493 | 5 years ago |
Nicolas Werner | 4d20839d74 | 5 years ago |
Nicolas Werner | 9eddcfc42f | 5 years ago |
DeepBlueV7.X | fd270dcd55 | 5 years ago |
Nicolas Werner | 6b60ff7713 | 5 years ago |
Nicolas Werner | f452bdf2b0 | 5 years ago |
Nicolas Werner | 4ee9e5c27c | 5 years ago |
Nicolas Werner | 4e5bd53b13 | 5 years ago |
Nicolas Werner | fe45c49e56 | 5 years ago |
Nicolas Werner | 247539cb5a | 5 years ago |
Nicolas Werner | 937b35ca8a | 5 years ago |
Lorenzo Ancora | f7cd0c4137 | 5 years ago |
DeepBlueV7.X | fbcc53a632 | 5 years ago |
lkito | 60ad6ce277 | 5 years ago |
DeepBlueV7.X | 33ba81a2e6 | 5 years ago |
Joseph Donofry | e20cddd005 | 5 years ago |
Joseph Donofry | 5228861b88 | 5 years ago |
lkito | d8b89e2ef0 | 5 years ago |
Joseph Donofry | 1c521d1711 | 5 years ago |
Nicolas Werner | bdf1147a80 | 5 years ago |
Nicolas Werner | 23e4408fa8 | 5 years ago |
Nicolas Werner | 7de1fc62e8 | 5 years ago |
Nicolas Werner | 5abdad308d | 5 years ago |
Nicolas Werner | f0757a6426 | 5 years ago |
Nicolas Werner | a75d7f00bc | 5 years ago |
Joseph Donofry | f4ea0b215d | 5 years ago |
Joseph Donofry | a5778bdf40 | 5 years ago |
Joseph Donofry | fca85bea39 | 5 years ago |
Aaron Raimist | 3338ecd39f | 5 years ago |
Aaron Raimist | 14eaad355e | 5 years ago |
DeepBlueV7.X | 883567b0b9 | 5 years ago |
lkito | 2c21f6e3fa | 5 years ago |
Nicolas Werner | 3db9298e66 | 5 years ago |
Nicolas Werner | 73e0a9f453 | 5 years ago |
Nicolas Werner | 576269c9e5 | 5 years ago |
Joseph Donofry | ca20e2a98f | 5 years ago |
DeepBlueV7.X | ce1c6a7df3 | 5 years ago |
CH Chethan Reddy | 57b8cdbd4d | 5 years ago |
CH Chethan Reddy | e4a7e85935 | 5 years ago |
CH Chethan Reddy | 30d61e3114 | 5 years ago |
DeepBlueV7.X | 54e2e7ef7f | 5 years ago |
Nicolas Werner | 5a5b85cded | 5 years ago |
Nicolas Werner | 10f0f7462a | 5 years ago |
Joseph Donofry | 6d2789f4d5 | 5 years ago |
Joseph Donofry | eb4b02e8b9 | 5 years ago |
Joseph Donofry | 18f934efad | 5 years ago |
Nicolas Werner | 6ff002b4ed | 5 years ago |
Joseph Donofry | ff7468e6d5 | 5 years ago |
Nicolas Werner | 279bcd1bf2 | 5 years ago |
Nicolas Werner | 2c3d09edbb | 5 years ago |
Nicolas Werner | d6981355d3 | 5 years ago |
Joseph Donofry | dfb76c693d | 5 years ago |
Joseph Donofry | ee4dcef90f | 5 years ago |
Nicolas Werner | 004d10bfee | 5 years ago |
Joseph Donofry | 8984661187 | 5 years ago |
Nicolas Werner | 000ab4853a | 5 years ago |
Nicolas Werner | 6befadeec8 | 5 years ago |
Nicolas Werner | 9713284435 | 5 years ago |
Nicolas Werner | 15716f5a34 | 5 years ago |
Nicolas Werner | 7b1fa60cc6 | 5 years ago |
Nicolas Werner | 813790e603 | 5 years ago |
DeepBlueV7.X | 197f702dd0 | 5 years ago |
jonnius | 5f9ce33562 | 5 years ago |
Joseph Donofry | 991fa4ac98 | 5 years ago |
Joseph Donofry | 5b9611dcd3 | 5 years ago |
Joseph Donofry | cc92161339 | 5 years ago |
Joseph Donofry | a1661f7006 | 5 years ago |
DeepBlueV7.X | 7beaf868ef | 5 years ago |
Nicolas Werner | a942ae9b93 | 5 years ago |
Nicolas Werner | 0b1d3a40f4 | 5 years ago |
Nicolas Werner | e48dfd15fe | 5 years ago |
Nicolas Werner | e5a5a66716 | 5 years ago |
Nicolas Werner | ff54ce9334 | 5 years ago |
Nicolas Werner | e045e3eb1c | 5 years ago |
Nicolas Werner | 8348a6c35d | 5 years ago |
Nicolas Werner | a1951056da | 5 years ago |
Nicolas Werner | e55a09906f | 5 years ago |
Nicolas Werner | ca5490074a | 5 years ago |
Nicolas Werner | 692c6119b4 | 5 years ago |
Nicolas Werner | db24f174b1 | 5 years ago |
Nicolas Werner | 846ff33ed8 | 5 years ago |
Nicolas Werner | 54013e4a00 | 5 years ago |
Nicolas Werner | a9aed09d35 | 5 years ago |
Nicolas Werner | a8b22e49c3 | 5 years ago |
Nicolas Werner | 0c73c74574 | 5 years ago |
DeepBlueV7.X | ccd55c70df | 5 years ago |
Nicolas Werner | d6685e8d61 | 5 years ago |
Nicolas Werner | dad2de7ba2 | 5 years ago |
DeepBlueV7.X | 38417a374d | 5 years ago |
Lasath Fernando | 166ed0674d | 5 years ago |
Nicolas Werner | 9750241e73 | 5 years ago |
DeepBlueV7.X | 00c4d2629a | 5 years ago |
Nicolas Werner | 641a883bfd | 5 years ago |
Nicolas Werner | e6fcccc8bd | 5 years ago |
Nicolas Werner | 2997155f56 | 5 years ago |
Nicolas Werner | 0716bbafff | 5 years ago |
Lasath Fernando | 21b33363ce | 5 years ago |
Lasath Fernando | 1f9d3024b1 | 5 years ago |
DeepBlueV7.X | 19ec8d261d | 5 years ago |
Nicolas Werner | 3226d1787c | 5 years ago |
Nicolas Werner | bb66254c58 | 5 years ago |
Nicolas Werner | 27350cf51e | 5 years ago |
Lasath Fernando | c5d373e8f1 | 5 years ago |
Lasath Fernando | 5c57de070e | 5 years ago |
Lasath Fernando | 84c5ff0bcb | 5 years ago |
Lasath Fernando | 1555dc2296 | 5 years ago |
Nicolas Werner | 17c657a170 | 5 years ago |
Nicolas Werner | dbb13dfad6 | 5 years ago |
Nicolas Werner | d6386546b3 | 5 years ago |
Nicolas Werner | 28adc9dc9b | 5 years ago |
Nicolas Werner | d94ac86816 | 5 years ago |
Joseph Donofry | a5d5ea1881 | 5 years ago |
Joseph Donofry | 14ce840649 | 5 years ago |
Joseph Donofry | 619d9bc185 | 5 years ago |
Joseph Donofry | 4e3b190ac7 | 5 years ago |
Joseph Donofry | 5893365439 | 5 years ago |
Joseph Donofry | 18557023d9 | 5 years ago |
Joseph Donofry | 87c06f0fc9 | 5 years ago |
Joseph Donofry | 994edb836c | 5 years ago |
Nicolas Werner | ddcab64315 | 5 years ago |
Joseph Donofry | 69b0b68f92 | 5 years ago |
Nicolas Werner | b00e624fb0 | 5 years ago |
Nicolas Werner | d5e1475a5c | 5 years ago |
Nicolas Werner | 4720d2b562 | 5 years ago |
Joseph Donofry | b298f01d92 | 5 years ago |
Nicolas Werner | 35601b6bef | 5 years ago |
Joseph Donofry | a4c280a4f9 | 5 years ago |
Nicolas Werner | 0e1bb5137b | 5 years ago |
Nicolas Werner | 69c8476cb7 | 5 years ago |
Nicolas Werner | ba3d405513 | 5 years ago |
Nicolas Werner | 6a39b29acc | 5 years ago |
Nicolas Werner | e8271acd99 | 5 years ago |
DeepBlueV7.X | 22ecdfc3ff | 5 years ago |
Lorem | b4622f510e | 5 years ago |
Joseph Donofry | 03a838709b | 5 years ago |
Joe | 0768a70e75 | 5 years ago |
Joe | 9d46eb123f | 5 years ago |
Nicolas Werner | 6e1c57a702 | 5 years ago |
Nicolas Werner | eff8af6fac | 5 years ago |
DeepBlueV7.X | 076a1c3607 | 5 years ago |
Jason Volk | 1eede32a8b | 5 years ago |
Nicolas Werner | 695c3c0814 | 5 years ago |
Nicolas Werner | 70b0cbcd0f | 5 years ago |
Nicolas Werner | a32bf3d4fc | 5 years ago |
Nicolas Werner | dd1dca3751 | 5 years ago |
Nicolas Werner | a70044e67b | 5 years ago |
Nicolas Werner | c739a0422d | 5 years ago |
Nicolas Werner | 277c0e9564 | 5 years ago |
DeepBlueV7.X | 46f3c97879 | 5 years ago |
DeepBlueV7.X | 741da4ab32 | 5 years ago |
Nicolas Werner | 872507002a | 5 years ago |
Nicolas Werner | 3ae4e49b5f | 5 years ago |
Nicolas Werner | dcddea6fb8 | 5 years ago |
Nicolas Werner | 82ec022f9c | 5 years ago |
Nicolas Werner | 2b24a978e1 | 5 years ago |
Nicolas Werner | 2022775dd0 | 5 years ago |
Nicolas Werner | 537cc966cc | 5 years ago |
Nicolas Werner | d68b24188f | 5 years ago |
Nicolas Werner | d2e495532d | 5 years ago |
adasauce | 5f7f564e57 | 5 years ago |
Nicolas Werner | 4fa12b52aa | 5 years ago |
Nicolas Werner | 6794b6472d | 5 years ago |
Nicolas Werner | 31a5972f2a | 5 years ago |
Nicolas Werner | 94f5b25888 | 5 years ago |
Nicolas Werner | 8bf25f3d80 | 5 years ago |
Nicolas Werner | d35cc3dda4 | 5 years ago |
DeepBlueV7.X | 384ef13c76 | 5 years ago |
Cadence Ember | fd1825b7c0 | 5 years ago |
DeepBlueV7.X | e389588bc2 | 5 years ago |
Emi Simpson | a9bbea881e | 5 years ago |
DeepBlueV7.X | 0913887546 | 5 years ago |
abma | 5e14bbd1cd | 5 years ago |
Nicolas Werner | 95272ce4ab | 5 years ago |
Nicolas Werner | 7a2b996f18 | 5 years ago |
Nicolas Werner | 7df6529310 | 5 years ago |
Nicolas Werner | b25e481418 | 5 years ago |
Nicolas Werner | 31a83c515e | 5 years ago |
DeepBlueV7.X | c32a8bc226 | 5 years ago |
Adasauce | d0a1e81f43 | 5 years ago |
DeepBlueV7.X | b41e2e6f18 | 5 years ago |
Emi Simpson | 5c308b1caf | 5 years ago |
Emi Simpson | 06676cfb33 | 5 years ago |
Emi Simpson | d51cbe7e32 | 5 years ago |
Emi Simpson | 78ac902549 | 5 years ago |
Emi Simpson | 12aa94ad9a | 5 years ago |
Emi Simpson | bf5ae884de | 5 years ago |
Emi Simpson | abac4c8d34 | 5 years ago |
Emi Simpson | 2320bfea26 | 5 years ago |
Emi Simpson | 6bbe8ade0f | 5 years ago |
Emi Simpson | b6bd36ac16 | 5 years ago |
Emi Simpson | 81c9cb5c79 | 5 years ago |
Emi Simpson | a5b388db15 | 5 years ago |
Emi Simpson | 389117f1e8 | 5 years ago |
Emi Simpson | 0153dc7a39 | 5 years ago |
Emi Simpson | b2a6232eb3 | 5 years ago |
Emi Simpson | 08125e8c44 | 5 years ago |
DeepBlueV7.X | 5e991af57f | 5 years ago |
Pushpam Choudhary | 93bcdbed2d | 5 years ago |
Pushpam Choudhary | 71ba8cb284 | 5 years ago |
Nicolas Werner | 0f1a352dd7 | 5 years ago |
Nicolas Werner | 69a50c15c7 | 5 years ago |
Nicolas Werner | a071f55c7b | 5 years ago |
DeepBlueV7.X | baaa687d33 | 5 years ago |
DeepBlueV7.X | 50eec6a96b | 5 years ago |
Emi Simpson | 3fddf69958 | 5 years ago |
DeepBlueV7.X | fc2f08a186 | 5 years ago |
Nicolas Werner | 31a403a68e | 5 years ago |
Nicolas Werner | b894ce4dcd | 5 years ago |
Nicolas Werner | 5ac18f1f5f | 5 years ago |
adasauce | 005ed00d67 | 5 years ago |
Adasauce | ee176de1ec | 5 years ago |
Adasauce | ed4bb1a576 | 5 years ago |
Joseph Donofry | ca1d3203f5 | 5 years ago |
Joseph Donofry | 1046958099 | 5 years ago |
Joe | 0f9c9df0a9 | 5 years ago |
DeepBlueV7.X | 7da5361995 | 5 years ago |
Nicolas Werner | 0fc98b2692 | 5 years ago |
Adasauce | 309461dcba | 5 years ago |
Nicolas Werner | 328a3c7ebd | 5 years ago |
DeepBlueV7.X | 1a2b8b7a6b | 5 years ago |
Joseph Donofry | 30cb7c5b02 | 5 years ago |
Joseph Donofry | 326e327d2c | 5 years ago |
Nicolas Werner | 2fd6385403 | 5 years ago |
Nicolas Werner | b80697b072 | 5 years ago |
Nicolas Werner | 095b59c01f | 5 years ago |
Nicolas Werner | 9efa001bcf | 5 years ago |
Nicolas Werner | 11bffd5d90 | 5 years ago |
Nicolas Werner | b1eff0a37f | 5 years ago |
Nicolas Werner | 4d79f8078e | 5 years ago |
Nicolas Werner | 505a8b5742 | 5 years ago |
Nicolas Werner | bed8c52891 | 5 years ago |
Nicolas Werner | 5115339636 | 5 years ago |
Nicolas Werner | 1eb2869fa8 | 5 years ago |
Nicolas Werner | 62b962cb44 | 5 years ago |
DeepBlueV7.X | f0db1c5f12 | 5 years ago |
nixo | 3c57c322d3 | 5 years ago |
Nicolas Werner | 653ad0be97 | 5 years ago |
DeepBlueV7.X | 58c5bdd7b1 | 5 years ago |
DeepBlueV7.X | 17063c4f39 | 5 years ago |
Nicolas Werner | f3a2fdb7c9 | 5 years ago |
Nicolas Werner | 33f9b9672a | 5 years ago |
Nicolas Werner | e96241265e | 5 years ago |
Nicolas Werner | 3ef0d9db3c | 5 years ago |
DeepBlueV7.X | fee00746c8 | 5 years ago |
Adasauce | 6e6fe0cad2 | 5 years ago |
DeepBlueV7.X | 1de7efd4ea | 5 years ago |
Protesilaos Stavrou | eb61ca8796 | 5 years ago |
Tulir | 7277df5591 | 5 years ago |
@ -0,0 +1,21 @@ |
|||||||
|
$file = "nheko_win_64.zip" |
||||||
|
$fileName = "nheko-${env:APPVEYOR_REPO_BRANCH}-${env:APPVEYOR_REPO_COMMIT}-win64.zip" |
||||||
|
|
||||||
|
$response = Invoke-RestMethod -uri "https://matrix.neko.dev/_matrix/media/r0/upload?filename=$fileName" -Method Post -Infile "$file" -ContentType 'application/x-compressed' -Headers @{"Authorization"="Bearer ${env:MATRIX_ACCESS_TOKEN}"} |
||||||
|
|
||||||
|
$txId = [DateTimeOffset]::Now.ToUnixTimeSeconds() |
||||||
|
$fileSize = (Get-Item $file).Length |
||||||
|
$body = @{ |
||||||
|
"body" = "${fileName}" |
||||||
|
"filename"= "${fileName}" |
||||||
|
"info" = @{ |
||||||
|
"mimetype" = "application/x-compressed" |
||||||
|
"size" = ${fileSize} |
||||||
|
} |
||||||
|
"msgtype" = "m.file" |
||||||
|
"url" = ${response}.content_uri |
||||||
|
} | ConvertTo-Json |
||||||
|
$room = "!TshDrgpBNBDmfDeEGN:neko.dev" |
||||||
|
|
||||||
|
Invoke-RestMethod -uri "https://matrix.neko.dev/_matrix/client/r0/rooms/${room}/send/m.room.message/${txid}" -Method Put -Body "$body" -ContentType 'application/json' -Headers @{"Authorization"="Bearer ${env:MATRIX_ACCESS_TOKEN}"} |
||||||
|
|
@ -0,0 +1,9 @@ |
|||||||
|
#!/bin/sh |
||||||
|
|
||||||
|
file=$(find artifacts/ -type f -exec basename {} \;) |
||||||
|
fileName="nheko-${TRAVIS_BRANCH}-${file#nheko-}" |
||||||
|
|
||||||
|
uri=$(curl -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" -H "Content-Type: application/x-compressed" -X POST --data-binary "@artifacts/${file}" "https://matrix.neko.dev/_matrix/media/r0/upload?filename=${fileName}" --http1.1 | python -c "import sys, json; print(json.load(sys.stdin)['content_uri'])") |
||||||
|
echo "Uploaded to ${uri}" |
||||||
|
|
||||||
|
curl -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" -H "Content-Type: application/json" -X PUT -d "{ \"body\": \"${fileName}\", \"filename\": \"${fileName}\", \"info\": { \"mimetype\": \"application/x-compressed\", \"size\": $(wc -c < artifacts/${file}) }, \"msgtype\": \"m.file\", \"url\": \"${uri}\" }" "https://matrix.neko.dev/_matrix/client/r0/rooms/${ROOM}/send/m.room.message/$(date +%s)" |
@ -0,0 +1,20 @@ |
|||||||
|
--- |
||||||
|
name: Feature request |
||||||
|
about: Suggest an idea for this project |
||||||
|
title: '' |
||||||
|
labels: enhancement |
||||||
|
assignees: '' |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.** |
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
||||||
|
|
||||||
|
**Describe the solution you'd like** |
||||||
|
A clear and concise description of what you want to happen. |
||||||
|
|
||||||
|
**Describe alternatives you've considered** |
||||||
|
A clear and concise description of any alternative solutions or features you've considered. |
||||||
|
|
||||||
|
**Additional context** |
||||||
|
Add any other context or screenshots about the feature request here. |
@ -1,5 +1,5 @@ |
|||||||
hunter_config( |
hunter_config( |
||||||
Boost |
Boost |
||||||
VERSION "1.70.0-p0" |
VERSION "1.70.0-p1" |
||||||
CMAKE_ARGS IOSTREAMS_NO_BZIP2=1 |
CMAKE_ARGS IOSTREAMS_NO_BZIP2=1 |
||||||
) |
) |
||||||
|
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 759 B |
After Width: | Height: | Size: 573 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 385 B |
After Width: | Height: | Size: 741 B |
@ -0,0 +1 @@ |
|||||||
|
*.qm |
@ -0,0 +1,5 @@ |
|||||||
|
The below media files were obtained from https://github.com/matrix-org/matrix-react-sdk/tree/develop/res/media |
||||||
|
|
||||||
|
callend.ogg |
||||||
|
ringback.ogg |
||||||
|
ring.ogg |
@ -1,56 +1,67 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="UTF-8"?> |
||||||
<!-- Copyright 2019 mujx, nheko reborn developers --> |
<!-- Copyright 2020 mujx, nheko reborn developers --> |
||||||
<component type="desktop"> |
<component type="desktop"> |
||||||
<id>nheko.desktop</id> |
<id>nheko.desktop</id> |
||||||
<metadata_license>CC0-1.0</metadata_license> |
<metadata_license>CC0-1.0</metadata_license> |
||||||
<project_license>GPL-3.0-or-later and CC-BY</project_license> |
<project_license>GPL-3.0-or-later and CC-BY</project_license> |
||||||
<name>nheko</name> |
<name>nheko</name> |
||||||
<summary>Desktop client for the Matrix protocol</summary> |
<summary>Desktop client for the Matrix protocol</summary> |
||||||
<description> |
<description> |
||||||
<p>The motivation behind the project is to provide a native |
<p>The motivation behind the project is to provide a native |
||||||
desktop app for Matrix that feels more like a mainstream |
desktop app for Matrix that feels more like a mainstream |
||||||
chat app.</p> |
chat app.</p> |
||||||
</description> |
</description> |
||||||
<translation/> |
<translation/> |
||||||
<languages> |
<languages> |
||||||
<lang>de</lang> |
<lang>de</lang> |
||||||
<lang>el</lang> |
<lang>el</lang> |
||||||
<lang>en</lang> |
<lang>en</lang> |
||||||
<lang>fr</lang> |
<lang>fr</lang> |
||||||
<lang>nl</lang> |
<lang>nl</lang> |
||||||
<lang>pl</lang> |
<lang>pl</lang> |
||||||
<lang>ru</lang> |
<lang>ru</lang> |
||||||
<lang>zh_CN</lang> |
<lang>zh_CN</lang> |
||||||
</languages> |
</languages> |
||||||
<content_rating type="oars-1.0"> |
<content_rating type="oars-1.0"> |
||||||
<content_attribute id="social-chat">intense</content_attribute> |
<content_attribute id="social-chat">intense</content_attribute> |
||||||
<content_attribute id="social-audio">intense</content_attribute> |
<content_attribute id="social-audio">intense</content_attribute> |
||||||
</content_rating> |
</content_rating> |
||||||
<screenshots> |
<screenshots> |
||||||
<screenshot type="default"> |
<screenshot type="default"> |
||||||
<image>https://nheko-reborn.github.io/images/screenshots/chat-resized.png</image> |
<image>https://nheko-reborn.github.io/images/screenshots/chat-resized.png</image> |
||||||
</screenshot> |
</screenshot> |
||||||
<screenshot> |
<screenshot> |
||||||
<image>https://nheko-reborn.github.io/images/screenshots/Start-resized.png</image> |
<image>https://nheko-reborn.github.io/images/screenshots/Start-resized.png</image> |
||||||
</screenshot> |
</screenshot> |
||||||
<screenshot> |
<screenshot> |
||||||
<image>https://nheko-reborn.github.io/images/screenshots/settings-resized.png</image> |
<image>https://nheko-reborn.github.io/images/screenshots/settings-resized.png</image> |
||||||
</screenshot> |
</screenshot> |
||||||
<screenshot> |
<screenshot> |
||||||
<image>https://nheko-reborn.github.io/images/screenshots/login-resized.png</image> |
<image>https://nheko-reborn.github.io/images/screenshots/login-resized.png</image> |
||||||
</screenshot> |
</screenshot> |
||||||
</screenshots> |
</screenshots> |
||||||
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url> |
<url type="homepage">https://github.com/Nheko-Reborn/nheko</url> |
||||||
<update_contact>https://github.com/Nheko-Reborn</update_contact> |
<update_contact>https://github.com/Nheko-Reborn</update_contact> |
||||||
<releases> |
<releases> |
||||||
<release version="0.6.4" date="2019-05-22" /> |
<release date="2020-06-12" version="0.7.2"/> |
||||||
<release version="0.6.3" date="2019-02-08" /> |
<release date="2020-04-24" version="0.7.1"/> |
||||||
<release version="0.6.2" date="2018-10-07" /> |
<release date="2020-04-19" version="0.7.0"/> |
||||||
<release version="0.6.1" date="2018-09-26" /> |
<release date="2019-05-22" version="0.6.4"/> |
||||||
<release version="0.6.0" date="2018-09-21" /> |
<release date="2019-02-08" version="0.6.3"/> |
||||||
<release version="0.5.5" date="2018-09-01" /> |
<release date="2018-10-07" version="0.6.2"/> |
||||||
<release version="0.5.4" date="2018-08-21" /> |
<release date="2018-09-26" version="0.6.1"/> |
||||||
<release version="0.5.3" date="2018-08-12" /> |
<release date="2018-09-21" version="0.6.0"/> |
||||||
<release version="0.5.2" date="2018-07-28" /> |
<release date="2018-09-01" version="0.5.5"/> |
||||||
</releases> |
<release date="2018-08-21" version="0.5.4"/> |
||||||
|
<release date="2018-08-12" version="0.5.3"/> |
||||||
|
<release date="2018-07-28" version="0.5.2"/> |
||||||
|
</releases> |
||||||
|
|
||||||
|
<developer_name>Nheko Reborn</developer_name> |
||||||
|
|
||||||
|
<url type="bugtracker">https://github.com/Nheko-Reborn/nheko/issues</url> |
||||||
|
|
||||||
|
<url type="help">https://github.com/Nheko-Reborn/nheko/</url> |
||||||
|
|
||||||
|
<url type="translate">https://weblate.nheko.im/projects/nheko/</url> |
||||||
</component> |
</component> |
||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
@ -0,0 +1,113 @@ |
|||||||
|
import QtQuick 2.9 |
||||||
|
import QtQuick.Controls 2.3 |
||||||
|
import QtQuick.Layouts 1.2 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: activeCallBar |
||||||
|
|
||||||
|
visible: TimelineManager.callState != WebRTCState.DISCONNECTED |
||||||
|
color: "#2ECC71" |
||||||
|
implicitHeight: rowLayout.height + 8 |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
id: rowLayout |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.verticalCenter: parent.verticalCenter |
||||||
|
anchors.leftMargin: 8 |
||||||
|
|
||||||
|
Avatar { |
||||||
|
width: avatarSize |
||||||
|
height: avatarSize |
||||||
|
url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") |
||||||
|
displayName: TimelineManager.callPartyName |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
font.pointSize: fontMetrics.font.pointSize * 1.1 |
||||||
|
text: " " + TimelineManager.callPartyName + " " |
||||||
|
} |
||||||
|
|
||||||
|
Image { |
||||||
|
Layout.preferredWidth: 24 |
||||||
|
Layout.preferredHeight: 24 |
||||||
|
source: "qrc:/icons/icons/ui/place-call.png" |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
id: callStateLabel |
||||||
|
|
||||||
|
font.pointSize: fontMetrics.font.pointSize * 1.1 |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
function onCallStateChanged(state) { |
||||||
|
switch (state) { |
||||||
|
case WebRTCState.INITIATING: |
||||||
|
callStateLabel.text = qsTr("Initiating..."); |
||||||
|
break; |
||||||
|
case WebRTCState.OFFERSENT: |
||||||
|
callStateLabel.text = qsTr("Calling..."); |
||||||
|
break; |
||||||
|
case WebRTCState.CONNECTING: |
||||||
|
callStateLabel.text = qsTr("Connecting..."); |
||||||
|
break; |
||||||
|
case WebRTCState.CONNECTED: |
||||||
|
callStateLabel.text = "00:00"; |
||||||
|
var d = new Date(); |
||||||
|
callTimer.startTime = Math.floor(d.getTime() / 1000); |
||||||
|
break; |
||||||
|
case WebRTCState.DISCONNECTED: |
||||||
|
callStateLabel.text = ""; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
target: TimelineManager |
||||||
|
} |
||||||
|
|
||||||
|
Timer { |
||||||
|
id: callTimer |
||||||
|
|
||||||
|
property int startTime |
||||||
|
|
||||||
|
function pad(n) { |
||||||
|
return (n < 10) ? ("0" + n) : n; |
||||||
|
} |
||||||
|
|
||||||
|
interval: 1000 |
||||||
|
running: TimelineManager.callState == WebRTCState.CONNECTED |
||||||
|
repeat: true |
||||||
|
onTriggered: { |
||||||
|
var d = new Date(); |
||||||
|
let seconds = Math.floor(d.getTime() / 1000 - startTime); |
||||||
|
let s = Math.floor(seconds % 60); |
||||||
|
let m = Math.floor(seconds / 60) % 60; |
||||||
|
let h = Math.floor(seconds / 3600); |
||||||
|
callStateLabel.text = (h ? (pad(h) + ":") : "") + pad(m) + ":" + pad(s); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
width: 24 |
||||||
|
height: 24 |
||||||
|
buttonTextColor: "#000000" |
||||||
|
image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png" |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") |
||||||
|
onClicked: TimelineManager.toggleMicMute() |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
implicitWidth: 16 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,52 +1,75 @@ |
|||||||
import QtQuick 2.6 |
|
||||||
import QtGraphicalEffects 1.0 |
import QtGraphicalEffects 1.0 |
||||||
import Qt.labs.settings 1.0 |
import QtQuick 2.6 |
||||||
|
import QtQuick.Controls 2.3 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
Rectangle { |
Rectangle { |
||||||
id: avatar |
id: avatar |
||||||
width: 48 |
|
||||||
height: 48 |
property alias url: img.source |
||||||
radius: settings.avatar_circles ? height/2 : 3 |
property string userid |
||||||
|
property string displayName |
||||||
Settings { |
|
||||||
id: settings |
width: 48 |
||||||
category: "user" |
height: 48 |
||||||
property bool avatar_circles: true |
radius: Settings.avatarCircles ? height / 2 : 3 |
||||||
} |
color: colors.base |
||||||
|
|
||||||
property alias url: img.source |
Label { |
||||||
property string displayName |
anchors.fill: parent |
||||||
|
text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") |
||||||
Text { |
textFormat: Text.RichText |
||||||
anchors.fill: parent |
font.pixelSize: avatar.height / 2 |
||||||
text: chat.model.escapeEmoji(String.fromCodePoint(displayName.codePointAt(0))) |
verticalAlignment: Text.AlignVCenter |
||||||
textFormat: Text.RichText |
horizontalAlignment: Text.AlignHCenter |
||||||
color: colors.text |
visible: img.status != Image.Ready |
||||||
font.pixelSize: avatar.height/2 |
color: colors.text |
||||||
verticalAlignment: Text.AlignVCenter |
} |
||||||
horizontalAlignment: Text.AlignHCenter |
|
||||||
} |
Image { |
||||||
|
id: img |
||||||
Image { |
|
||||||
id: img |
anchors.fill: parent |
||||||
anchors.fill: parent |
asynchronous: true |
||||||
asynchronous: true |
fillMode: Image.PreserveAspectCrop |
||||||
fillMode: Image.PreserveAspectCrop |
mipmap: true |
||||||
mipmap: true |
smooth: false |
||||||
smooth: false |
sourceSize.width: avatar.width |
||||||
|
sourceSize.height: avatar.height |
||||||
sourceSize.width: avatar.width |
layer.enabled: true |
||||||
sourceSize.height: avatar.height |
|
||||||
|
layer.effect: OpacityMask { |
||||||
layer.enabled: true |
|
||||||
layer.effect: OpacityMask { |
maskSource: Rectangle { |
||||||
maskSource: Rectangle { |
anchors.fill: parent |
||||||
anchors.fill: parent |
width: avatar.width |
||||||
width: avatar.width |
height: avatar.height |
||||||
height: avatar.height |
radius: Settings.avatarCircles ? height / 2 : 3 |
||||||
radius: settings.avatar_circles ? height/2 : 3 |
} |
||||||
} |
|
||||||
} |
} |
||||||
} |
|
||||||
color: colors.base |
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
anchors.bottom: avatar.bottom |
||||||
|
anchors.right: avatar.right |
||||||
|
visible: !!userid |
||||||
|
height: avatar.height / 6 |
||||||
|
width: height |
||||||
|
radius: Settings.avatarCircles ? height / 2 : height / 4 |
||||||
|
color: { |
||||||
|
switch (TimelineManager.userPresence(userid)) { |
||||||
|
case "online": |
||||||
|
return "#00cc66"; |
||||||
|
case "unavailable": |
||||||
|
return "#ff9933"; |
||||||
|
case "offline": |
||||||
|
default: |
||||||
|
// return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled |
||||||
|
"transparent"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
} |
} |
||||||
|
@ -1,29 +1,30 @@ |
|||||||
import QtQuick 2.3 |
import QtQuick 2.3 |
||||||
import QtQuick.Controls 2.3 |
import QtQuick.Controls 2.3 |
||||||
|
|
||||||
Button { |
AbstractButton { |
||||||
property string image: undefined |
id: button |
||||||
|
|
||||||
id: button |
property string image: undefined |
||||||
|
property color highlightColor: colors.highlight |
||||||
flat: true |
property color buttonTextColor: colors.buttonText |
||||||
|
|
||||||
// disable background, because we don't want a border on hover |
width: 16 |
||||||
background: Item { |
height: 16 |
||||||
} |
|
||||||
|
Image { |
||||||
Image { |
id: buttonImg |
||||||
id: buttonImg |
|
||||||
// Workaround, can't get icon.source working for now... |
// Workaround, can't get icon.source working for now... |
||||||
anchors.fill: parent |
anchors.fill: parent |
||||||
source: "image://colorimage/" + image + "?" + (button.hovered ? colors.highlight : colors.buttonText) |
source: "image://colorimage/" + image + "?" + (button.hovered ? highlightColor : buttonTextColor) |
||||||
} |
} |
||||||
|
|
||||||
MouseArea |
MouseArea { |
||||||
{ |
id: mouseArea |
||||||
id: mouseArea |
|
||||||
anchors.fill: parent |
anchors.fill: parent |
||||||
onPressed: mouse.accepted = false |
onPressed: mouse.accepted = false |
||||||
cursorShape: Qt.PointingHandCursor |
cursorShape: Qt.PointingHandCursor |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
@ -1,31 +1,37 @@ |
|||||||
import QtQuick 2.5 |
import QtQuick 2.5 |
||||||
import QtQuick.Controls 2.3 |
import QtQuick.Controls 2.3 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
TextEdit { |
TextEdit { |
||||||
textFormat: TextEdit.RichText |
textFormat: TextEdit.RichText |
||||||
readOnly: true |
readOnly: true |
||||||
wrapMode: Text.Wrap |
wrapMode: Text.Wrap |
||||||
selectByMouse: true |
selectByMouse: true |
||||||
color: colors.text |
activeFocusOnPress: false |
||||||
|
color: colors.text |
||||||
|
onLinkActivated: { |
||||||
|
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) { |
||||||
|
chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]); |
||||||
|
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) { |
||||||
|
TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]); |
||||||
|
} else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { |
||||||
|
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link); |
||||||
|
TimelineManager.setHistoryView(match[1]); |
||||||
|
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain); |
||||||
|
} else { |
||||||
|
TimelineManager.openLink(link); |
||||||
|
} |
||||||
|
} |
||||||
|
ToolTip.visible: hoveredLink |
||||||
|
ToolTip.text: hoveredLink |
||||||
|
|
||||||
onLinkActivated: { |
MouseArea { |
||||||
if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) |
id: ma |
||||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) |
|
||||||
else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { |
anchors.fill: parent |
||||||
var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) |
propagateComposedEvents: true |
||||||
timelineManager.setHistoryView(match[1]) |
acceptedButtons: Qt.NoButton |
||||||
chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) |
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor |
||||||
} |
} |
||||||
else Qt.openUrlExternally(link) |
|
||||||
} |
|
||||||
MouseArea |
|
||||||
{ |
|
||||||
id: ma |
|
||||||
anchors.fill: parent |
|
||||||
onPressed: mouse.accepted = false |
|
||||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor |
|
||||||
} |
|
||||||
|
|
||||||
ToolTip.visible: hoveredLink |
|
||||||
ToolTip.text: hoveredLink |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,95 @@ |
|||||||
|
import QtQuick 2.6 |
||||||
|
import QtQuick.Controls 2.2 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
// This class is for showing Reactions in the timeline row, not for |
||||||
|
// adding new reactions via the emoji picker |
||||||
|
Flow { |
||||||
|
id: reactionFlow |
||||||
|
|
||||||
|
// highlight colors for selfReactedEvent background |
||||||
|
property real highlightHue: colors.highlight.hslHue |
||||||
|
property real highlightSat: colors.highlight.hslSaturation |
||||||
|
property real highlightLight: colors.highlight.hslLightness |
||||||
|
property string eventId |
||||||
|
property alias reactions: repeater.model |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
spacing: 4 |
||||||
|
|
||||||
|
Repeater { |
||||||
|
id: repeater |
||||||
|
|
||||||
|
delegate: AbstractButton { |
||||||
|
id: reaction |
||||||
|
|
||||||
|
hoverEnabled: true |
||||||
|
implicitWidth: contentItem.childrenRect.width + contentItem.leftPadding * 2 |
||||||
|
implicitHeight: contentItem.childrenRect.height |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: modelData.users |
||||||
|
onClicked: { |
||||||
|
console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent); |
||||||
|
TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key); |
||||||
|
} |
||||||
|
|
||||||
|
contentItem: Row { |
||||||
|
anchors.centerIn: parent |
||||||
|
spacing: reactionText.implicitHeight / 4 |
||||||
|
leftPadding: reactionText.implicitHeight / 2 |
||||||
|
rightPadding: reactionText.implicitHeight / 2 |
||||||
|
|
||||||
|
TextMetrics { |
||||||
|
id: textMetrics |
||||||
|
|
||||||
|
font.family: Settings.emojiFont |
||||||
|
elide: Text.ElideRight |
||||||
|
elideWidth: 150 |
||||||
|
text: modelData.key |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
id: reactionText |
||||||
|
|
||||||
|
anchors.baseline: reactionCounter.baseline |
||||||
|
text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") |
||||||
|
font.family: Settings.emojiFont |
||||||
|
color: reaction.hovered ? colors.highlight : colors.text |
||||||
|
maximumLineCount: 1 |
||||||
|
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: divider |
||||||
|
|
||||||
|
height: Math.floor(reactionCounter.implicitHeight * 1.4) |
||||||
|
width: 1 |
||||||
|
color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
id: reactionCounter |
||||||
|
|
||||||
|
anchors.verticalCenter: divider.verticalCenter |
||||||
|
text: modelData.count |
||||||
|
font: reaction.font |
||||||
|
color: reaction.hovered ? colors.highlight : colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
background: Rectangle { |
||||||
|
anchors.centerIn: parent |
||||||
|
implicitWidth: reaction.implicitWidth |
||||||
|
implicitHeight: reaction.implicitHeight |
||||||
|
border.color: (reaction.hovered || modelData.selfReactedEvent !== '') ? colors.highlight : colors.text |
||||||
|
color: modelData.selfReactedEvent !== '' ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : colors.base |
||||||
|
border.width: 1 |
||||||
|
radius: reaction.height / 2 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net> |
||||||
|
* Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com> |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU General Public License as published by |
||||||
|
* the Free Software Foundation; either version 2 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along |
||||||
|
* with this program; if not, write to the Free Software Foundation, Inc., |
||||||
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||||
|
*/ |
||||||
|
/* |
||||||
|
* Shamelessly stolen from: |
||||||
|
* https://cgit.kde.org/kube.git/tree/framework/qml/ScrollHelper.qml |
||||||
|
* |
||||||
|
* The MouseArea + interactive: false + maximumFlickVelocity are required |
||||||
|
* to fix scrolling for desktop systems where we don't want flicking behaviour. |
||||||
|
* |
||||||
|
* See also: |
||||||
|
* ScrollView.qml in qtquickcontrols |
||||||
|
* qquickwheelarea.cpp in qtquickcontrols |
||||||
|
*/ |
||||||
|
|
||||||
|
import QtQuick 2.9 |
||||||
|
import QtQuick.Controls 2.3 |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
// console.warn("Delta: ", wheel.pixelDelta.y); |
||||||
|
// console.warn("Old position: ", flickable.contentY); |
||||||
|
// console.warn("New position: ", newPos); |
||||||
|
|
||||||
|
id: root |
||||||
|
|
||||||
|
property Flickable flickable |
||||||
|
property alias enabled: root.enabled |
||||||
|
|
||||||
|
function calculateNewPosition(flickableItem, wheel) { |
||||||
|
//Nothing to scroll |
||||||
|
if (flickableItem.contentHeight < flickableItem.height) |
||||||
|
return flickableItem.contentY; |
||||||
|
|
||||||
|
//Ignore 0 events (happens at least with Christians trackpad) |
||||||
|
if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) |
||||||
|
return flickableItem.contentY; |
||||||
|
|
||||||
|
//pixelDelta seems to be the same as angleDelta/8 |
||||||
|
var pixelDelta = 0; |
||||||
|
//The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta |
||||||
|
if (wheel.angleDelta.y) { |
||||||
|
var wheelScrollLines = 3; //Default value of QApplication wheelScrollLines property |
||||||
|
var pixelPerLine = 20; //Default value in Qt, originally comes from QTextEdit |
||||||
|
var ticks = (wheel.angleDelta.y / 8) / 15; //Divide by 8 gives us pixels typically come in 15pixel steps. |
||||||
|
pixelDelta = ticks * pixelPerLine * wheelScrollLines; |
||||||
|
} else { |
||||||
|
pixelDelta = wheel.pixelDelta.y; |
||||||
|
} |
||||||
|
pixelDelta = Math.round(pixelDelta); |
||||||
|
if (!pixelDelta) |
||||||
|
return flickableItem.contentY; |
||||||
|
|
||||||
|
var minYExtent = flickableItem.originY + flickableItem.topMargin; |
||||||
|
var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; |
||||||
|
if (typeof (flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) |
||||||
|
minYExtent += flickableItem.headerItem.height; |
||||||
|
|
||||||
|
//Avoid overscrolling |
||||||
|
return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); |
||||||
|
} |
||||||
|
|
||||||
|
propagateComposedEvents: true |
||||||
|
//Place the mouse area under the flickable |
||||||
|
z: -1 |
||||||
|
onFlickableChanged: { |
||||||
|
if (enabled) { |
||||||
|
flickable.maximumFlickVelocity = 100000; |
||||||
|
flickable.boundsBehavior = Flickable.StopAtBounds; |
||||||
|
root.parent = flickable; |
||||||
|
} |
||||||
|
} |
||||||
|
acceptedButtons: Qt.NoButton |
||||||
|
onWheel: { |
||||||
|
var newPos = calculateNewPosition(flickable, wheel); |
||||||
|
// Show the scrollbars |
||||||
|
flickable.flick(0, 0); |
||||||
|
flickable.contentY = newPos; |
||||||
|
cancelFlickStateTimer.start(); |
||||||
|
} |
||||||
|
|
||||||
|
Timer { |
||||||
|
id: cancelFlickStateTimer |
||||||
|
|
||||||
|
//How long the scrollbar will remain visible |
||||||
|
interval: 500 |
||||||
|
// Hide the scrollbars |
||||||
|
onTriggered: { |
||||||
|
flickable.cancelFlick(); |
||||||
|
flickable.movementEnded(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,98 +1,148 @@ |
|||||||
|
import "./delegates" |
||||||
|
import "./emoji" |
||||||
import QtQuick 2.6 |
import QtQuick 2.6 |
||||||
import QtQuick.Controls 2.3 |
import QtQuick.Controls 2.3 |
||||||
import QtQuick.Layouts 1.2 |
import QtQuick.Layouts 1.2 |
||||||
import QtQuick.Window 2.2 |
import QtQuick.Window 2.2 |
||||||
|
|
||||||
import im.nheko 1.0 |
import im.nheko 1.0 |
||||||
|
|
||||||
import "./delegates" |
Item { |
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
height: row.height |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
propagateComposedEvents: true |
||||||
|
preventStealing: true |
||||||
|
hoverEnabled: true |
||||||
|
acceptedButtons: Qt.AllButtons |
||||||
|
onClicked: { |
||||||
|
if (mouse.button === Qt.RightButton) |
||||||
|
messageContextMenu.show(model.id, model.type, model.isEncrypted, row); |
||||||
|
|
||||||
|
} |
||||||
|
onPressAndHold: { |
||||||
|
messageContextMenu.show(model.id, model.type, model.isEncrypted, row, mapToItem(timelineRoot, mouse.x, mouse.y)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" |
||||||
|
anchors.fill: row |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
id: row |
||||||
|
|
||||||
|
anchors.leftMargin: avatarSize + 16 |
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
|
||||||
|
Column { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.alignment: Qt.AlignTop |
||||||
|
spacing: 4 |
||||||
|
|
||||||
|
// fancy reply, if this is a reply |
||||||
|
Reply { |
||||||
|
visible: model.replyTo |
||||||
|
modelData: chat.model.getDump(model.replyTo, model.id) |
||||||
|
userColor: TimelineManager.userColor(modelData.userId, colors.window) |
||||||
|
} |
||||||
|
|
||||||
|
// actual message content |
||||||
|
MessageDelegate { |
||||||
|
id: contentItem |
||||||
|
|
||||||
|
width: parent.width |
||||||
|
modelData: model |
||||||
|
} |
||||||
|
|
||||||
|
Reactions { |
||||||
|
id: reactionRow |
||||||
|
|
||||||
|
reactions: model.reactions |
||||||
|
eventId: model.id |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
StatusIndicator { |
||||||
|
state: model.state |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
width: 16 |
||||||
|
} |
||||||
|
|
||||||
|
EncryptionIndicator { |
||||||
|
visible: model.isRoomEncrypted |
||||||
|
encrypted: model.isEncrypted |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
width: 16 |
||||||
|
} |
||||||
|
|
||||||
|
EmojiButton { |
||||||
|
id: reactButton |
||||||
|
|
||||||
|
visible: Settings.buttonsInTimeline |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
width: 16 |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("React") |
||||||
|
emojiPicker: emojiPopup |
||||||
|
event_id: model.id |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: replyButton |
||||||
|
|
||||||
|
visible: Settings.buttonsInTimeline |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
width: 16 |
||||||
|
hoverEnabled: true |
||||||
|
image: ":/icons/icons/ui/mail-reply.png" |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Reply") |
||||||
|
onClicked: chat.model.replyAction(model.id) |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: optionsButton |
||||||
|
|
||||||
|
visible: Settings.buttonsInTimeline |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
width: 16 |
||||||
|
hoverEnabled: true |
||||||
|
image: ":/icons/icons/ui/vertical-ellipsis.png" |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Options") |
||||||
|
onClicked: messageContextMenu.show(model.id, model.type, model.isEncrypted, optionsButton) |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||||
|
text: model.timestamp.toLocaleTimeString("HH:mm") |
||||||
|
width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) |
||||||
|
color: inactiveColors.text |
||||||
|
ToolTip.visible: ma.containsMouse |
||||||
|
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
id: ma |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
hoverEnabled: true |
||||||
|
propagateComposedEvents: true |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
RowLayout { |
|
||||||
property var view: chat |
|
||||||
|
|
||||||
anchors.leftMargin: avatarSize + 4 |
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
|
|
||||||
Column { |
|
||||||
Layout.fillWidth: true |
|
||||||
Layout.alignment: Qt.AlignTop |
|
||||||
spacing: 4 |
|
||||||
|
|
||||||
// fancy reply, if this is a reply |
|
||||||
Reply { |
|
||||||
visible: model.replyTo |
|
||||||
modelData: chat.model.getDump(model.replyTo) |
|
||||||
userColor: timelineManager.userColor(modelData.userId, colors.window) |
|
||||||
} |
|
||||||
|
|
||||||
// actual message content |
|
||||||
MessageDelegate { |
|
||||||
id: contentItem |
|
||||||
|
|
||||||
width: parent.width |
|
||||||
|
|
||||||
modelData: model |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
StatusIndicator { |
|
||||||
state: model.state |
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
|
||||||
Layout.preferredHeight: 16 |
|
||||||
width: 16 |
|
||||||
} |
|
||||||
|
|
||||||
EncryptionIndicator { |
|
||||||
visible: model.isEncrypted |
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
|
||||||
Layout.preferredHeight: 16 |
|
||||||
width: 16 |
|
||||||
} |
|
||||||
|
|
||||||
ImageButton { |
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
|
||||||
Layout.preferredHeight: 16 |
|
||||||
width: 16 |
|
||||||
id: replyButton |
|
||||||
hoverEnabled: true |
|
||||||
|
|
||||||
|
|
||||||
image: ":/icons/icons/ui/mail-reply.png" |
|
||||||
|
|
||||||
ToolTip.visible: hovered |
|
||||||
ToolTip.text: qsTr("Reply") |
|
||||||
|
|
||||||
onClicked: view.model.replyAction(model.id) |
|
||||||
} |
|
||||||
ImageButton { |
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
|
||||||
Layout.preferredHeight: 16 |
|
||||||
width: 16 |
|
||||||
id: optionsButton |
|
||||||
hoverEnabled: true |
|
||||||
|
|
||||||
image: ":/icons/icons/ui/vertical-ellipsis.png" |
|
||||||
|
|
||||||
ToolTip.visible: hovered |
|
||||||
ToolTip.text: qsTr("Options") |
|
||||||
|
|
||||||
onClicked: messageContextMenu.show(model.id, model.type, optionsButton) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
Text { |
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
|
||||||
text: model.timestamp.toLocaleTimeString("HH:mm") |
|
||||||
color: inactiveColors.text |
|
||||||
|
|
||||||
MouseArea{ |
|
||||||
id: ma |
|
||||||
anchors.fill: parent |
|
||||||
hoverEnabled: true |
|
||||||
} |
|
||||||
|
|
||||||
ToolTip.visible: ma.containsMouse |
|
||||||
ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate) |
|
||||||
} |
|
||||||
} |
} |
||||||
|
@ -1,309 +1,582 @@ |
|||||||
|
import "./delegates" |
||||||
|
import "./device-verification" |
||||||
|
import "./emoji" |
||||||
|
import QtGraphicalEffects 1.0 |
||||||
import QtQuick 2.9 |
import QtQuick 2.9 |
||||||
import QtQuick.Controls 2.3 |
import QtQuick.Controls 2.3 |
||||||
import QtQuick.Layouts 1.2 |
import QtQuick.Layouts 1.2 |
||||||
import QtGraphicalEffects 1.0 |
|
||||||
import QtQuick.Window 2.2 |
import QtQuick.Window 2.2 |
||||||
|
|
||||||
import im.nheko 1.0 |
import im.nheko 1.0 |
||||||
|
import im.nheko.EmojiModel 1.0 |
||||||
|
|
||||||
|
Page { |
||||||
|
id: timelineRoot |
||||||
|
|
||||||
|
property var colors: currentActivePalette |
||||||
|
property var systemInactive |
||||||
|
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive |
||||||
|
property int avatarSize: 40 |
||||||
|
property real highlightHue: colors.highlight.hslHue |
||||||
|
property real highlightSat: colors.highlight.hslSaturation |
||||||
|
property real highlightLight: colors.highlight.hslLightness |
||||||
|
|
||||||
import "./delegates" |
palette: colors |
||||||
|
|
||||||
|
FontMetrics { |
||||||
|
id: fontMetrics |
||||||
|
} |
||||||
|
|
||||||
|
EmojiPicker { |
||||||
|
id: emojiPopup |
||||||
|
|
||||||
|
width: 7 * 52 + 20 |
||||||
|
height: 6 * 52 |
||||||
|
colors: palette |
||||||
|
|
||||||
|
model: EmojiProxyModel { |
||||||
|
category: EmojiCategory.People |
||||||
|
|
||||||
|
sourceModel: EmojiModel { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Menu { |
||||||
|
id: messageContextMenu |
||||||
|
|
||||||
|
property string eventId |
||||||
|
property int eventType |
||||||
|
property bool isEncrypted |
||||||
|
|
||||||
|
function show(eventId_, eventType_, isEncrypted_, showAt_, position) { |
||||||
|
eventId = eventId_; |
||||||
|
eventType = eventType_; |
||||||
|
isEncrypted = isEncrypted_; |
||||||
|
if (position) |
||||||
|
popup(position, showAt_); |
||||||
|
else |
||||||
|
popup(showAt_); |
||||||
|
} |
||||||
|
|
||||||
|
modal: true |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("React") |
||||||
|
onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Reply") |
||||||
|
onClicked: chat.model.replyAction(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Read receipts") |
||||||
|
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Mark as read") |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("View raw message") |
||||||
|
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
visible: messageContextMenu.isEncrypted |
||||||
|
height: visible ? implicitHeight : 0 |
||||||
|
text: qsTr("View decrypted raw message") |
||||||
|
onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Redact message") |
||||||
|
onTriggered: chat.model.redactEvent(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker |
||||||
|
height: visible ? implicitHeight : 0 |
||||||
|
text: qsTr("Save as") |
||||||
|
onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
anchors.fill: parent |
||||||
|
color: colors.window |
||||||
|
|
||||||
|
Component { |
||||||
|
id: deviceVerificationDialog |
||||||
|
|
||||||
|
DeviceVerification { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) { |
||||||
|
var dialog = deviceVerificationDialog.createObject(timelineRoot, { |
||||||
|
"flow": flow |
||||||
|
}); |
||||||
|
dialog.show(); |
||||||
|
} |
||||||
|
|
||||||
|
target: TimelineManager |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
function onOpenProfile(profile) { |
||||||
|
var userProfile = userProfileComponent.createObject(timelineRoot, { |
||||||
|
"profile": profile |
||||||
|
}); |
||||||
|
userProfile.show(); |
||||||
|
} |
||||||
|
|
||||||
|
target: TimelineManager.timeline |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
visible: !TimelineManager.timeline && !TimelineManager.isInitialSync |
||||||
|
anchors.centerIn: parent |
||||||
|
text: qsTr("No room open") |
||||||
|
font.pointSize: 24 |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
BusyIndicator { |
||||||
|
visible: running |
||||||
|
anchors.centerIn: parent |
||||||
|
running: TimelineManager.isInitialSync |
||||||
|
height: 200 |
||||||
|
width: 200 |
||||||
|
z: 3 |
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
anchors.fill: parent |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: topBar |
||||||
|
|
||||||
|
Layout.fillWidth: true |
||||||
|
implicitHeight: topLayout.height + 16 |
||||||
|
z: 3 |
||||||
|
color: colors.base |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: TimelineManager.openRoomSettings() |
||||||
|
} |
||||||
|
|
||||||
|
GridLayout { |
||||||
|
//Layout.margins: 8 |
||||||
|
|
||||||
|
id: topLayout |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.margins: 8 |
||||||
|
anchors.verticalCenter: parent.verticalCenter |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: backToRoomsButton |
||||||
|
|
||||||
|
Layout.column: 0 |
||||||
|
Layout.row: 0 |
||||||
|
Layout.rowSpan: 2 |
||||||
|
Layout.alignment: Qt.AlignVCenter |
||||||
|
visible: TimelineManager.isNarrowView |
||||||
|
image: ":/icons/icons/ui/angle-pointing-to-left.png" |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Back to room list") |
||||||
|
onClicked: TimelineManager.backToRooms() |
||||||
|
} |
||||||
|
|
||||||
|
Avatar { |
||||||
|
Layout.column: 1 |
||||||
|
Layout.row: 0 |
||||||
|
Layout.rowSpan: 2 |
||||||
|
Layout.alignment: Qt.AlignVCenter |
||||||
|
width: avatarSize |
||||||
|
height: avatarSize |
||||||
|
url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : "" |
||||||
|
displayName: chat.model ? chat.model.roomName : qsTr("No room selected") |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: TimelineManager.openRoomSettings() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.column: 2 |
||||||
|
Layout.row: 0 |
||||||
|
color: colors.text |
||||||
|
font.pointSize: fontMetrics.font.pointSize * 1.1 |
||||||
|
text: chat.model ? chat.model.roomName : qsTr("No room selected") |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: TimelineManager.openRoomSettings() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
MatrixText { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.column: 2 |
||||||
|
Layout.row: 1 |
||||||
|
Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines |
||||||
|
clip: true |
||||||
|
text: chat.model ? chat.model.roomTopic : "" |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: roomOptionsButton |
||||||
|
|
||||||
|
Layout.column: 3 |
||||||
|
Layout.row: 0 |
||||||
|
Layout.rowSpan: 2 |
||||||
|
Layout.alignment: Qt.AlignVCenter |
||||||
|
image: ":/icons/icons/ui/vertical-ellipsis.png" |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Room options") |
||||||
|
onClicked: roomOptionsMenu.popup(roomOptionsButton) |
||||||
|
|
||||||
|
Menu { |
||||||
|
id: roomOptionsMenu |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Invite users") |
||||||
|
onTriggered: TimelineManager.openInviteUsersDialog() |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Members") |
||||||
|
onTriggered: TimelineManager.openMemberListDialog() |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Leave room") |
||||||
|
onTriggered: TimelineManager.openLeaveRoomDialog() |
||||||
|
} |
||||||
|
|
||||||
|
MenuItem { |
||||||
|
text: qsTr("Settings") |
||||||
|
onTriggered: TimelineManager.openRoomSettings() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ListView { |
||||||
|
id: chat |
||||||
|
|
||||||
|
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2) |
||||||
|
|
||||||
|
visible: TimelineManager.timeline != null |
||||||
|
cacheBuffer: 400 |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.fillHeight: true |
||||||
|
model: TimelineManager.timeline |
||||||
|
boundsBehavior: Flickable.StopAtBounds |
||||||
|
pixelAligned: true |
||||||
|
spacing: 4 |
||||||
|
verticalLayoutDirection: ListView.BottomToTop |
||||||
|
onCountChanged: { |
||||||
|
if (atYEnd) |
||||||
|
model.currentIndex = 0; |
||||||
|
|
||||||
|
} // Mark last event as read, since we are at the bottom |
||||||
|
|
||||||
|
ScrollHelper { |
||||||
|
flickable: parent |
||||||
|
anchors.fill: parent |
||||||
|
} |
||||||
|
|
||||||
|
Shortcut { |
||||||
|
sequence: StandardKey.MoveToPreviousPage |
||||||
|
onActivated: { |
||||||
|
chat.contentY = chat.contentY - chat.height / 2; |
||||||
|
chat.returnToBounds(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Shortcut { |
||||||
|
sequence: StandardKey.MoveToNextPage |
||||||
|
onActivated: { |
||||||
|
chat.contentY = chat.contentY + chat.height / 2; |
||||||
|
chat.returnToBounds(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Shortcut { |
||||||
|
sequence: StandardKey.Cancel |
||||||
|
onActivated: chat.model.reply = undefined |
||||||
|
} |
||||||
|
|
||||||
|
Shortcut { |
||||||
|
sequence: "Alt+Up" |
||||||
|
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0) |
||||||
|
} |
||||||
|
|
||||||
|
Shortcut { |
||||||
|
sequence: "Alt+Down" |
||||||
|
onActivated: { |
||||||
|
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1; |
||||||
|
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: userProfileComponent |
||||||
|
|
||||||
|
UserProfile { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
section { |
||||||
|
property: "section" |
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: sectionHeader |
||||||
|
|
||||||
|
Column { |
||||||
|
property var modelData |
||||||
|
property string section |
||||||
|
property string nextSection |
||||||
|
|
||||||
|
topPadding: 4 |
||||||
|
bottomPadding: 4 |
||||||
|
spacing: 8 |
||||||
|
visible: !!modelData |
||||||
|
width: parent.width |
||||||
|
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 |
||||||
|
|
||||||
|
Label { |
||||||
|
id: dateBubble |
||||||
|
|
||||||
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined |
||||||
|
visible: section.includes(" ") |
||||||
|
text: chat.model.formatDateSeparator(modelData.timestamp) |
||||||
|
color: colors.text |
||||||
|
height: fontMetrics.height * 1.4 |
||||||
|
width: contentWidth * 1.2 |
||||||
|
horizontalAlignment: Text.AlignHCenter |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
|
||||||
|
background: Rectangle { |
||||||
|
radius: parent.height / 2 |
||||||
|
color: colors.base |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Row { |
||||||
|
height: userName.height |
||||||
|
spacing: 8 |
||||||
|
|
||||||
|
Avatar { |
||||||
|
width: avatarSize |
||||||
|
height: avatarSize |
||||||
|
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") |
||||||
|
displayName: modelData.userName |
||||||
|
userid: modelData.userId |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: chat.model.openUserProfile(modelData.userId) |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
propagateComposedEvents: true |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
id: userName |
||||||
|
|
||||||
|
text: TimelineManager.escapeEmoji(modelData.userName) |
||||||
|
color: TimelineManager.userColor(modelData.userId, colors.window) |
||||||
|
textFormat: Text.RichText |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
onClicked: chat.model.openUserProfile(modelData.userId) |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
propagateComposedEvents: true |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { |
||||||
|
id: scrollbar |
||||||
|
} |
||||||
|
|
||||||
|
delegate: Item { |
||||||
|
id: wrapper |
||||||
|
|
||||||
|
// This would normally be previousSection, but our model's order is inverted. |
||||||
|
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 |
||||||
|
property Item section |
||||||
|
|
||||||
|
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined |
||||||
|
width: chat.delegateMaxWidth |
||||||
|
height: section ? section.height + timelinerow.height : timelinerow.height |
||||||
|
onSectionBoundaryChanged: { |
||||||
|
if (sectionBoundary) { |
||||||
|
var properties = { |
||||||
|
"modelData": model.dump, |
||||||
|
"section": ListView.section, |
||||||
|
"nextSection": ListView.nextSection |
||||||
|
}; |
||||||
|
section = sectionHeader.createObject(wrapper, properties); |
||||||
|
} else { |
||||||
|
section.destroy(); |
||||||
|
section = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
TimelineRow { |
||||||
|
id: timelinerow |
||||||
|
|
||||||
|
y: section ? section.y + section.height : 0 |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
function onMovementEnded() { |
||||||
|
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) |
||||||
|
chat.model.currentIndex = index; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
target: chat |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
footer: BusyIndicator { |
||||||
|
anchors.horizontalCenter: parent.horizontalCenter |
||||||
|
running: chat.model && chat.model.paginationInProgress |
||||||
|
height: 50 |
||||||
|
width: 50 |
||||||
|
z: 3 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
id: chatFooter |
||||||
|
|
||||||
|
implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height) |
||||||
|
Layout.fillWidth: true |
||||||
|
z: 3 |
||||||
|
|
||||||
|
Column { |
||||||
|
id: footerContent |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.bottom: parent.bottom |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: typingRect |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent" |
||||||
|
height: typingDisplay.height |
||||||
|
|
||||||
|
Label { |
||||||
|
id: typingDisplay |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.leftMargin: 10 |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.rightMargin: 10 |
||||||
|
color: colors.text |
||||||
|
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" |
||||||
|
textFormat: Text.RichText |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: replyPopup |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
visible: chat.model && chat.model.reply |
||||||
|
// Height of child, plus margins, plus border |
||||||
|
height: replyPreview.height + 10 |
||||||
|
color: colors.base |
||||||
|
|
||||||
|
Reply { |
||||||
|
id: replyPreview |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.leftMargin: 10 |
||||||
|
anchors.right: closeReplyButton.left |
||||||
|
anchors.rightMargin: 20 |
||||||
|
anchors.bottom: parent.bottom |
||||||
|
modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : { |
||||||
|
} |
||||||
|
userColor: TimelineManager.userColor(modelData.userId, colors.window) |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: closeReplyButton |
||||||
|
|
||||||
|
anchors.right: parent.right |
||||||
|
anchors.rightMargin: 15 |
||||||
|
anchors.top: replyPreview.top |
||||||
|
hoverEnabled: true |
||||||
|
width: 16 |
||||||
|
height: 16 |
||||||
|
image: ":/icons/icons/ui/remove-symbol.png" |
||||||
|
ToolTip.visible: closeReplyButton.hovered |
||||||
|
ToolTip.text: qsTr("Close") |
||||||
|
onClicked: chat.model.reply = undefined |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ActiveCallBar { |
||||||
|
Layout.fillWidth: true |
||||||
|
z: 3 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
systemInactive: SystemPalette { |
||||||
|
colorGroup: SystemPalette.Disabled |
||||||
|
} |
||||||
|
|
||||||
Item { |
|
||||||
property var colors: currentActivePalette |
|
||||||
property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled } |
|
||||||
property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive |
|
||||||
property int avatarSize: 40 |
|
||||||
|
|
||||||
Menu { |
|
||||||
id: messageContextMenu |
|
||||||
palette: colors |
|
||||||
modal: true |
|
||||||
|
|
||||||
function show(eventId_, eventType_, showAt) { |
|
||||||
eventId = eventId_ |
|
||||||
eventType = eventType_ |
|
||||||
popup(showAt) |
|
||||||
} |
|
||||||
|
|
||||||
property string eventId |
|
||||||
property int eventType |
|
||||||
|
|
||||||
MenuItem { |
|
||||||
text: qsTr("Read receipts") |
|
||||||
onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId) |
|
||||||
} |
|
||||||
MenuItem { |
|
||||||
text: qsTr("Mark as read") |
|
||||||
} |
|
||||||
MenuItem { |
|
||||||
text: qsTr("View raw message") |
|
||||||
onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId) |
|
||||||
} |
|
||||||
MenuItem { |
|
||||||
text: qsTr("Redact message") |
|
||||||
onTriggered: chat.model.redactEvent(messageContextMenu.eventId) |
|
||||||
} |
|
||||||
MenuItem { |
|
||||||
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker |
|
||||||
text: qsTr("Save as") |
|
||||||
onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
id: timelineRoot |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
anchors.fill: parent |
|
||||||
color: colors.window |
|
||||||
|
|
||||||
Text { |
|
||||||
visible: !timelineManager.timeline && !timelineManager.isInitialSync |
|
||||||
anchors.centerIn: parent |
|
||||||
text: qsTr("No room open") |
|
||||||
font.pointSize: 24 |
|
||||||
color: colors.windowText |
|
||||||
} |
|
||||||
|
|
||||||
BusyIndicator { |
|
||||||
anchors.centerIn: parent |
|
||||||
running: timelineManager.isInitialSync |
|
||||||
height: 200 |
|
||||||
width: 200 |
|
||||||
z: 3 |
|
||||||
} |
|
||||||
|
|
||||||
ListView { |
|
||||||
id: chat |
|
||||||
|
|
||||||
visible: timelineManager.timeline != null |
|
||||||
|
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
anchors.top: parent.top |
|
||||||
anchors.bottom: chatFooter.top |
|
||||||
|
|
||||||
anchors.leftMargin: 4 |
|
||||||
anchors.rightMargin: scrollbar.width |
|
||||||
|
|
||||||
model: timelineManager.timeline |
|
||||||
|
|
||||||
boundsBehavior: Flickable.StopAtBounds |
|
||||||
pixelAligned: true |
|
||||||
|
|
||||||
MouseArea { |
|
||||||
anchors.fill: parent |
|
||||||
acceptedButtons: Qt.NoButton |
|
||||||
propagateComposedEvents: true |
|
||||||
z: -1 |
|
||||||
onWheel: { |
|
||||||
if (wheel.angleDelta != 0) { |
|
||||||
chat.contentY = chat.contentY - wheel.angleDelta.y |
|
||||||
wheel.accepted = true |
|
||||||
chat.returnToBounds() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Shortcut { |
|
||||||
sequence: StandardKey.MoveToPreviousPage |
|
||||||
onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); } |
|
||||||
} |
|
||||||
Shortcut { |
|
||||||
sequence: StandardKey.MoveToNextPage |
|
||||||
onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); } |
|
||||||
} |
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar { |
|
||||||
id: scrollbar |
|
||||||
parent: chat.parent |
|
||||||
anchors.top: chat.top |
|
||||||
anchors.left: chat.right |
|
||||||
anchors.bottom: chat.bottom |
|
||||||
} |
|
||||||
|
|
||||||
spacing: 4 |
|
||||||
verticalLayoutDirection: ListView.BottomToTop |
|
||||||
|
|
||||||
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom |
|
||||||
|
|
||||||
delegate: Rectangle { |
|
||||||
// This would normally be previousSection, but our model's order is inverted. |
|
||||||
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 |
|
||||||
|
|
||||||
id: wrapper |
|
||||||
property Item section |
|
||||||
width: chat.width |
|
||||||
height: section ? section.height + timelinerow.height : timelinerow.height |
|
||||||
color: "transparent" |
|
||||||
|
|
||||||
TimelineRow { |
|
||||||
id: timelinerow |
|
||||||
y: section ? section.y + section.height : 0 |
|
||||||
} |
|
||||||
|
|
||||||
onSectionBoundaryChanged: { |
|
||||||
if (sectionBoundary) { |
|
||||||
var properties = { |
|
||||||
'modelData': model.dump, |
|
||||||
'section': ListView.section, |
|
||||||
'nextSection': ListView.nextSection |
|
||||||
} |
|
||||||
section = sectionHeader.createObject(wrapper, properties) |
|
||||||
} else { |
|
||||||
section.destroy() |
|
||||||
section = null |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Binding { |
|
||||||
target: chat.model |
|
||||||
property: "currentIndex" |
|
||||||
when: y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height |
|
||||||
value: index |
|
||||||
delayed: true |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
section { |
|
||||||
property: "section" |
|
||||||
} |
|
||||||
Component { |
|
||||||
id: sectionHeader |
|
||||||
Column { |
|
||||||
property var modelData |
|
||||||
property string section |
|
||||||
property string nextSection |
|
||||||
|
|
||||||
topPadding: 4 |
|
||||||
bottomPadding: 4 |
|
||||||
spacing: 8 |
|
||||||
|
|
||||||
visible: !!modelData |
|
||||||
|
|
||||||
width: parent.width |
|
||||||
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8 |
|
||||||
|
|
||||||
Label { |
|
||||||
id: dateBubble |
|
||||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined |
|
||||||
visible: section.includes(" ") |
|
||||||
text: chat.model.formatDateSeparator(modelData.timestamp) |
|
||||||
color: colors.windowText |
|
||||||
|
|
||||||
height: contentHeight * 1.2 |
|
||||||
width: contentWidth * 1.2 |
|
||||||
horizontalAlignment: Text.AlignHCenter |
|
||||||
background: Rectangle { |
|
||||||
radius: parent.height / 2 |
|
||||||
color: colors.base |
|
||||||
} |
|
||||||
} |
|
||||||
Row { |
|
||||||
height: userName.height |
|
||||||
spacing: 4 |
|
||||||
Avatar { |
|
||||||
width: avatarSize |
|
||||||
height: avatarSize |
|
||||||
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") |
|
||||||
displayName: modelData.userName |
|
||||||
|
|
||||||
MouseArea { |
|
||||||
anchors.fill: parent |
|
||||||
onClicked: chat.model.openUserProfile(modelData.userId) |
|
||||||
cursorShape: Qt.PointingHandCursor |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Text { |
|
||||||
id: userName |
|
||||||
text: chat.model.escapeEmoji(modelData.userName) |
|
||||||
color: timelineManager.userColor(modelData.userId, colors.window) |
|
||||||
textFormat: Text.RichText |
|
||||||
|
|
||||||
MouseArea { |
|
||||||
anchors.fill: parent |
|
||||||
onClicked: chat.model.openUserProfile(section.split(" ")[0]) |
|
||||||
cursorShape: Qt.PointingHandCursor |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
id: chatFooter |
|
||||||
|
|
||||||
height: Math.max(16, footerContent.height) |
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
anchors.bottom: parent.bottom |
|
||||||
z: 3 |
|
||||||
|
|
||||||
color: "transparent" |
|
||||||
|
|
||||||
Column { |
|
||||||
id: footerContent |
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
|
|
||||||
Text { |
|
||||||
id: typingDisplay |
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
anchors.leftMargin: 10 |
|
||||||
anchors.rightMargin: 10 |
|
||||||
|
|
||||||
text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : "" |
|
||||||
textFormat: Text.RichText |
|
||||||
color: colors.windowText |
|
||||||
} |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
anchors.left: parent.left |
|
||||||
anchors.right: parent.right |
|
||||||
|
|
||||||
id: replyPopup |
|
||||||
|
|
||||||
visible: timelineManager.replyingEvent && chat.model |
|
||||||
// Height of child, plus margins, plus border |
|
||||||
height: replyPreview.height + 10 |
|
||||||
color: colors.base |
|
||||||
|
|
||||||
|
|
||||||
Reply { |
|
||||||
id: replyPreview |
|
||||||
|
|
||||||
anchors.left: parent.left |
|
||||||
anchors.leftMargin: 10 |
|
||||||
anchors.right: closeReplyButton.left |
|
||||||
anchors.rightMargin: 20 |
|
||||||
anchors.bottom: parent.bottom |
|
||||||
|
|
||||||
modelData: chat.model ? chat.model.getDump(timelineManager.replyingEvent) : {} |
|
||||||
userColor: timelineManager.userColor(modelData.userId, colors.window) |
|
||||||
} |
|
||||||
|
|
||||||
ImageButton { |
|
||||||
id: closeReplyButton |
|
||||||
|
|
||||||
anchors.right: parent.right |
|
||||||
anchors.rightMargin: 15 |
|
||||||
anchors.top: replyPreview.top |
|
||||||
hoverEnabled: true |
|
||||||
width: 16 |
|
||||||
height: 16 |
|
||||||
|
|
||||||
image: ":/icons/icons/ui/remove-symbol.png" |
|
||||||
ToolTip.visible: closeReplyButton.hovered |
|
||||||
ToolTip.text: qsTr("Close") |
|
||||||
|
|
||||||
onClicked: timelineManager.closeReply() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,175 @@ |
|||||||
|
import "./device-verification" |
||||||
|
import QtQuick 2.9 |
||||||
|
import QtQuick.Controls 2.3 |
||||||
|
import QtQuick.Layouts 1.2 |
||||||
|
import QtQuick.Window 2.3 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
ApplicationWindow { |
||||||
|
id: userProfileDialog |
||||||
|
|
||||||
|
property var profile |
||||||
|
|
||||||
|
height: 650 |
||||||
|
width: 420 |
||||||
|
minimumHeight: 420 |
||||||
|
palette: colors |
||||||
|
|
||||||
|
Component { |
||||||
|
id: deviceVerificationDialog |
||||||
|
|
||||||
|
DeviceVerification { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: contentL |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
anchors.margins: 10 |
||||||
|
spacing: 10 |
||||||
|
|
||||||
|
Avatar { |
||||||
|
url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") |
||||||
|
height: 130 |
||||||
|
width: 130 |
||||||
|
displayName: profile.displayName |
||||||
|
userid: profile.userid |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
text: profile.displayName |
||||||
|
fontSizeMode: Text.HorizontalFit |
||||||
|
font.pixelSize: 20 |
||||||
|
color: TimelineManager.userColor(profile.userid, colors.window) |
||||||
|
font.bold: true |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
} |
||||||
|
|
||||||
|
MatrixText { |
||||||
|
text: profile.userid |
||||||
|
font.pixelSize: 15 |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
id: verifyUserButton |
||||||
|
|
||||||
|
text: qsTr("Verify") |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
enabled: !profile.isUserVerified |
||||||
|
visible: !profile.isUserVerified |
||||||
|
onClicked: profile.verify() |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
spacing: 8 |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
image: ":/icons/icons/ui/do-not-disturb-rounded-sign.png" |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Ban the user") |
||||||
|
onClicked: profile.banUser() |
||||||
|
} |
||||||
|
// ImageButton{ |
||||||
|
|
||||||
|
// image:":/icons/icons/ui/volume-off-indicator.png" |
||||||
|
// Layout.margins: { |
||||||
|
// left: 5 |
||||||
|
// right: 5 |
||||||
|
// } |
||||||
|
// ToolTip.visible: hovered |
||||||
|
// ToolTip.text: qsTr("Ignore messages from this user") |
||||||
|
// onClicked : { |
||||||
|
// profile.ignoreUser() |
||||||
|
// } |
||||||
|
// } |
||||||
|
ImageButton { |
||||||
|
image: ":/icons/icons/ui/black-bubble-speech.png" |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Start a private chat") |
||||||
|
onClicked: profile.startChat() |
||||||
|
} |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
image: ":/icons/icons/ui/round-remove-button.png" |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.visible: hovered |
||||||
|
ToolTip.text: qsTr("Kick the user") |
||||||
|
onClicked: profile.kickUser() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ListView { |
||||||
|
id: devicelist |
||||||
|
|
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.minimumHeight: 200 |
||||||
|
Layout.fillWidth: true |
||||||
|
clip: true |
||||||
|
spacing: 8 |
||||||
|
boundsBehavior: Flickable.StopAtBounds |
||||||
|
model: profile.deviceList |
||||||
|
|
||||||
|
delegate: RowLayout { |
||||||
|
width: devicelist.width |
||||||
|
spacing: 4 |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 0 |
||||||
|
|
||||||
|
Text { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.alignment: Qt.AlignLeft |
||||||
|
elide: Text.ElideRight |
||||||
|
font.bold: true |
||||||
|
color: colors.text |
||||||
|
text: model.deviceId |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
elide: Text.ElideRight |
||||||
|
color: colors.text |
||||||
|
text: model.deviceName |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Image { |
||||||
|
Layout.preferredHeight: 16 |
||||||
|
Layout.preferredWidth: 16 |
||||||
|
source: ((model.verificationStatus == VerificationStatus.VERIFIED) ? "image://colorimage/:/icons/icons/ui/lock.png?green" : ((model.verificationStatus == VerificationStatus.UNVERIFIED) ? "image://colorimage/:/icons/icons/ui/unlock.png?yellow" : "image://colorimage/:/icons/icons/ui/unlock.png?red")) |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
id: verifyButton |
||||||
|
|
||||||
|
text: (model.verificationStatus != VerificationStatus.VERIFIED) ? "Verify" : "Unverify" |
||||||
|
onClicked: { |
||||||
|
if (model.verificationStatus == VerificationStatus.VERIFIED) |
||||||
|
profile.unverify(model.deviceId); |
||||||
|
else |
||||||
|
profile.verify(model.deviceId); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
footer: DialogButtonBox { |
||||||
|
standardButtons: DialogButtonBox.Ok |
||||||
|
onAccepted: userProfileDialog.close() |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,57 +1,75 @@ |
|||||||
import QtQuick 2.6 |
import QtQuick 2.6 |
||||||
import QtQuick.Layouts 1.2 |
import QtQuick.Layouts 1.2 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Item { |
||||||
|
height: row.height + 24 |
||||||
|
width: parent ? parent.width : undefined |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
id: row |
||||||
|
|
||||||
|
anchors.centerIn: parent |
||||||
|
width: parent.width - 24 |
||||||
|
spacing: 15 |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: button |
||||||
|
|
||||||
|
color: colors.light |
||||||
|
radius: 22 |
||||||
|
height: 44 |
||||||
|
width: 44 |
||||||
|
|
||||||
|
Image { |
||||||
|
id: img |
||||||
|
|
||||||
|
anchors.centerIn: parent |
||||||
|
source: "qrc:/icons/icons/ui/arrow-pointing-down.png" |
||||||
|
fillMode: Image.Pad |
||||||
|
} |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: TimelineManager.timeline.saveMedia(model.data.id) |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: col |
||||||
|
|
||||||
|
Text { |
||||||
|
id: filename |
||||||
|
|
||||||
|
Layout.fillWidth: true |
||||||
|
text: model.data.filename |
||||||
|
textFormat: Text.PlainText |
||||||
|
elide: Text.ElideRight |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
id: filesize |
||||||
|
|
||||||
|
Layout.fillWidth: true |
||||||
|
text: model.data.filesize |
||||||
|
textFormat: Text.PlainText |
||||||
|
elide: Text.ElideRight |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
color: colors.dark |
||||||
|
z: -1 |
||||||
|
radius: 10 |
||||||
|
height: row.height + 24 |
||||||
|
width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth)) |
||||||
|
} |
||||||
|
|
||||||
Rectangle { |
|
||||||
radius: 10 |
|
||||||
color: colors.base |
|
||||||
height: row.height + 24 |
|
||||||
width: parent ? parent.width : undefined |
|
||||||
|
|
||||||
RowLayout { |
|
||||||
id: row |
|
||||||
|
|
||||||
anchors.centerIn: parent |
|
||||||
width: parent.width - 24 |
|
||||||
|
|
||||||
spacing: 15 |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
id: button |
|
||||||
color: colors.light |
|
||||||
radius: 22 |
|
||||||
height: 44 |
|
||||||
width: 44 |
|
||||||
Image { |
|
||||||
id: img |
|
||||||
anchors.centerIn: parent |
|
||||||
|
|
||||||
source: "qrc:/icons/icons/ui/arrow-pointing-down.png" |
|
||||||
fillMode: Image.Pad |
|
||||||
|
|
||||||
} |
|
||||||
MouseArea { |
|
||||||
anchors.fill: parent |
|
||||||
onClicked: timelineManager.timeline.saveMedia(model.data.id) |
|
||||||
cursorShape: Qt.PointingHandCursor |
|
||||||
} |
|
||||||
} |
|
||||||
ColumnLayout { |
|
||||||
id: col |
|
||||||
|
|
||||||
Text { |
|
||||||
Layout.fillWidth: true |
|
||||||
text: model.data.body |
|
||||||
textFormat: Text.PlainText |
|
||||||
elide: Text.ElideRight |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
Text { |
|
||||||
Layout.fillWidth: true |
|
||||||
text: model.data.filesize |
|
||||||
textFormat: Text.PlainText |
|
||||||
elide: Text.ElideRight |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
} |
||||||
|
@ -1,28 +1,70 @@ |
|||||||
import QtQuick 2.6 |
import QtQuick 2.6 |
||||||
|
|
||||||
import im.nheko 1.0 |
import im.nheko 1.0 |
||||||
|
|
||||||
Item { |
Item { |
||||||
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width) |
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) |
||||||
property double tempHeight: tempWidth * model.data.proportionalHeight |
property double tempHeight: tempWidth * model.data.proportionalHeight |
||||||
|
property double divisor: model.isReply ? 4 : 2 |
||||||
|
property bool tooHigh: tempHeight > timelineRoot.height / divisor |
||||||
|
|
||||||
|
height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) |
||||||
|
width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) |
||||||
|
|
||||||
|
Image { |
||||||
|
id: blurhash |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
visible: img.status != Image.Ready |
||||||
|
source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + colors.buttonText) |
||||||
|
asynchronous: true |
||||||
|
fillMode: Image.PreserveAspectFit |
||||||
|
sourceSize.width: parent.width |
||||||
|
sourceSize.height: parent.height |
||||||
|
} |
||||||
|
|
||||||
|
Image { |
||||||
|
id: img |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
source: model.data.url.replace("mxc://", "image://MxcImage/") |
||||||
|
asynchronous: true |
||||||
|
fillMode: Image.PreserveAspectFit |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
id: mouseArea |
||||||
|
enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready |
||||||
|
hoverEnabled: true |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
id: overlay |
||||||
|
|
||||||
property bool tooHigh: tempHeight > timelineRoot.height / 2 |
anchors.fill: parent |
||||||
|
visible: mouseArea.containsMouse |
||||||
|
|
||||||
height: tooHigh ? timelineRoot.height / 2 : tempHeight |
Rectangle { |
||||||
width: tooHigh ? (timelineRoot.height / 2) / model.data.proportionalHeight : tempWidth |
id: container |
||||||
|
|
||||||
Image { |
width: parent.width |
||||||
id: img |
implicitHeight: imgcaption.implicitHeight |
||||||
anchors.fill: parent |
anchors.bottom: overlay.bottom |
||||||
|
color: colors.window |
||||||
|
opacity: 0.75 |
||||||
|
} |
||||||
|
|
||||||
source: model.data.url.replace("mxc://", "image://MxcImage/") |
Text { |
||||||
asynchronous: true |
id: imgcaption |
||||||
fillMode: Image.PreserveAspectFit |
|
||||||
|
|
||||||
MouseArea { |
anchors.fill: container |
||||||
enabled: model.data.type == MtxEvent.ImageMessage |
elide: Text.ElideMiddle |
||||||
anchors.fill: parent |
horizontalAlignment: Text.AlignHCenter |
||||||
onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id) |
verticalAlignment: Text.AlignVCenter |
||||||
} |
// See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530 |
||||||
} |
text: model.data.filename ? model.data.filename : model.data.body |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1,4 +1,6 @@ |
|||||||
TextMessage { |
TextMessage { |
||||||
font.italic: true |
font.italic: true |
||||||
color: inactiveColors.text |
color: colors.buttonText |
||||||
|
height: isReply ? Math.min(chat.height / 8, implicitHeight) : undefined |
||||||
|
clip: true |
||||||
} |
} |
||||||
|
@ -1,7 +1,7 @@ |
|||||||
import ".." |
import ".." |
||||||
|
|
||||||
MatrixText { |
MatrixText { |
||||||
text: qsTr("unimplemented event: ") + model.data.typeString |
text: qsTr("unimplemented event: ") + model.data.typeString |
||||||
width: parent ? parent.width : undefined |
width: parent ? parent.width : undefined |
||||||
color: inactiveColors.text |
color: inactiveColors.text |
||||||
} |
} |
||||||
|
@ -1,167 +1,216 @@ |
|||||||
|
import QtMultimedia 5.6 |
||||||
import QtQuick 2.6 |
import QtQuick 2.6 |
||||||
import QtQuick.Layouts 1.2 |
|
||||||
import QtQuick.Controls 2.1 |
import QtQuick.Controls 2.1 |
||||||
import QtMultimedia 5.6 |
import QtQuick.Layouts 1.2 |
||||||
|
|
||||||
import im.nheko 1.0 |
import im.nheko 1.0 |
||||||
|
|
||||||
Rectangle { |
Rectangle { |
||||||
id: bg |
id: bg |
||||||
radius: 10 |
|
||||||
color: colors.base |
|
||||||
height: content.height + 24 |
|
||||||
width: parent ? parent.width : undefined |
|
||||||
|
|
||||||
Column { |
|
||||||
id: content |
|
||||||
width: parent.width - 24 |
|
||||||
anchors.centerIn: parent |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
id: videoContainer |
|
||||||
visible: model.data.type == MtxEvent.VideoMessage |
|
||||||
width: Math.min(parent.width, model.data.width ? model.data.width : 400) // some media has 0 as size... |
|
||||||
height: width*model.data.proportionalHeight |
|
||||||
Image { |
|
||||||
anchors.fill: parent |
|
||||||
source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") |
|
||||||
asynchronous: true |
|
||||||
fillMode: Image.PreserveAspectFit |
|
||||||
|
|
||||||
VideoOutput { |
|
||||||
anchors.fill: parent |
|
||||||
fillMode: VideoOutput.PreserveAspectFit |
|
||||||
source: media |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
RowLayout { |
|
||||||
width: parent.width |
|
||||||
Text { |
|
||||||
id: positionText |
|
||||||
text: "--:--:--" |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
Slider { |
|
||||||
Layout.fillWidth: true |
|
||||||
id: progress |
|
||||||
value: media.position |
|
||||||
from: 0 |
|
||||||
to: media.duration |
|
||||||
|
|
||||||
onMoved: media.seek(value) |
|
||||||
//indeterminate: true |
|
||||||
function updatePositionTexts() { |
|
||||||
function formatTime(date) { |
|
||||||
var hh = date.getUTCHours(); |
|
||||||
var mm = date.getUTCMinutes(); |
|
||||||
var ss = date.getSeconds(); |
|
||||||
if (hh < 10) {hh = "0"+hh;} |
|
||||||
if (mm < 10) {mm = "0"+mm;} |
|
||||||
if (ss < 10) {ss = "0"+ss;} |
|
||||||
return hh+":"+mm+":"+ss; |
|
||||||
} |
|
||||||
positionText.text = formatTime(new Date(media.position)) |
|
||||||
durationText.text = formatTime(new Date(media.duration)) |
|
||||||
} |
|
||||||
onValueChanged: updatePositionTexts() |
|
||||||
|
|
||||||
palette: colors |
|
||||||
} |
|
||||||
Text { |
|
||||||
id: durationText |
|
||||||
text: "--:--:--" |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
RowLayout { |
|
||||||
width: parent.width |
|
||||||
|
|
||||||
spacing: 15 |
|
||||||
|
|
||||||
Rectangle { |
|
||||||
id: button |
|
||||||
color: colors.window |
|
||||||
radius: 22 |
|
||||||
height: 44 |
|
||||||
width: 44 |
|
||||||
Image { |
|
||||||
id: img |
|
||||||
anchors.centerIn: parent |
|
||||||
z: 3 |
|
||||||
|
|
||||||
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?"+colors.text |
|
||||||
fillMode: Image.Pad |
|
||||||
|
|
||||||
} |
|
||||||
MouseArea { |
|
||||||
anchors.fill: parent |
|
||||||
onClicked: { |
|
||||||
switch (button.state) { |
|
||||||
case "": timelineManager.timeline.cacheMedia(model.data.id); break; |
|
||||||
case "stopped": |
|
||||||
media.play(); console.log("play"); |
|
||||||
button.state = "playing" |
|
||||||
break |
|
||||||
case "playing": |
|
||||||
media.pause(); console.log("pause"); |
|
||||||
button.state = "stopped" |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
cursorShape: Qt.PointingHandCursor |
|
||||||
} |
|
||||||
MediaPlayer { |
|
||||||
id: media |
|
||||||
onError: console.log(errorString) |
|
||||||
onStatusChanged: if(status == MediaPlayer.Loaded) progress.updatePositionTexts() |
|
||||||
onStopped: button.state = "stopped" |
|
||||||
} |
|
||||||
|
|
||||||
Connections { |
|
||||||
target: timelineManager.timeline |
|
||||||
onMediaCached: { |
|
||||||
if (mxcUrl == model.data.url) { |
|
||||||
media.source = "file://" + cacheUrl |
|
||||||
button.state = "stopped" |
|
||||||
console.log("media loaded: " + mxcUrl + " at " + cacheUrl) |
|
||||||
} |
|
||||||
console.log("media cached: " + mxcUrl + " at " + cacheUrl) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
states: [ |
|
||||||
State { |
|
||||||
name: "stopped" |
|
||||||
PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/play-sign.png?"+colors.text } |
|
||||||
}, |
|
||||||
State { |
|
||||||
name: "playing" |
|
||||||
PropertyChanges { target: img; source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?"+colors.text } |
|
||||||
} |
|
||||||
] |
|
||||||
} |
|
||||||
ColumnLayout { |
|
||||||
id: col |
|
||||||
|
|
||||||
Text { |
|
||||||
Layout.fillWidth: true |
|
||||||
text: model.data.body |
|
||||||
textFormat: Text.PlainText |
|
||||||
elide: Text.ElideRight |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
Text { |
|
||||||
Layout.fillWidth: true |
|
||||||
text: model.data.filesize |
|
||||||
textFormat: Text.PlainText |
|
||||||
elide: Text.ElideRight |
|
||||||
color: colors.text |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
radius: 10 |
||||||
|
color: colors.dark |
||||||
|
height: Math.round(content.height + 24) |
||||||
|
width: parent ? parent.width : undefined |
||||||
|
|
||||||
|
Column { |
||||||
|
id: content |
||||||
|
|
||||||
|
width: parent.width - 24 |
||||||
|
anchors.centerIn: parent |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: videoContainer |
||||||
|
|
||||||
|
property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) |
||||||
|
property double tempHeight: tempWidth * model.data.proportionalHeight |
||||||
|
property double divisor: model.isReply ? 4 : 2 |
||||||
|
property bool tooHigh: tempHeight > timelineRoot.height / divisor |
||||||
|
|
||||||
|
visible: model.data.type == MtxEvent.VideoMessage |
||||||
|
height: tooHigh ? timelineRoot.height / divisor : tempHeight |
||||||
|
width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth |
||||||
|
|
||||||
|
Image { |
||||||
|
anchors.fill: parent |
||||||
|
source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/") |
||||||
|
asynchronous: true |
||||||
|
fillMode: Image.PreserveAspectFit |
||||||
|
|
||||||
|
VideoOutput { |
||||||
|
anchors.fill: parent |
||||||
|
fillMode: VideoOutput.PreserveAspectFit |
||||||
|
source: media |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
width: parent.width |
||||||
|
|
||||||
|
Text { |
||||||
|
id: positionText |
||||||
|
|
||||||
|
text: "--:--:--" |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Slider { |
||||||
|
id: progress |
||||||
|
|
||||||
|
//indeterminate: true |
||||||
|
function updatePositionTexts() { |
||||||
|
function formatTime(date) { |
||||||
|
var hh = date.getUTCHours(); |
||||||
|
var mm = date.getUTCMinutes(); |
||||||
|
var ss = date.getSeconds(); |
||||||
|
if (hh < 10) |
||||||
|
hh = "0" + hh; |
||||||
|
|
||||||
|
if (mm < 10) |
||||||
|
mm = "0" + mm; |
||||||
|
|
||||||
|
if (ss < 10) |
||||||
|
ss = "0" + ss; |
||||||
|
|
||||||
|
return hh + ":" + mm + ":" + ss; |
||||||
|
} |
||||||
|
|
||||||
|
positionText.text = formatTime(new Date(media.position)); |
||||||
|
durationText.text = formatTime(new Date(media.duration)); |
||||||
|
} |
||||||
|
|
||||||
|
Layout.fillWidth: true |
||||||
|
value: media.position |
||||||
|
from: 0 |
||||||
|
to: media.duration |
||||||
|
onMoved: media.seek(value) |
||||||
|
onValueChanged: updatePositionTexts() |
||||||
|
palette: colors |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
id: durationText |
||||||
|
|
||||||
|
text: "--:--:--" |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
width: parent.width |
||||||
|
spacing: 15 |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
id: button |
||||||
|
|
||||||
|
color: colors.window |
||||||
|
radius: 22 |
||||||
|
height: 44 |
||||||
|
width: 44 |
||||||
|
states: [ |
||||||
|
State { |
||||||
|
name: "stopped" |
||||||
|
|
||||||
|
PropertyChanges { |
||||||
|
target: img |
||||||
|
source: "image://colorimage/:/icons/icons/ui/play-sign.png?" + colors.text |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "playing" |
||||||
|
|
||||||
|
PropertyChanges { |
||||||
|
target: img |
||||||
|
source: "image://colorimage/:/icons/icons/ui/pause-symbol.png?" + colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
Image { |
||||||
|
id: img |
||||||
|
|
||||||
|
anchors.centerIn: parent |
||||||
|
z: 3 |
||||||
|
source: "image://colorimage/:/icons/icons/ui/arrow-pointing-down.png?" + colors.text |
||||||
|
fillMode: Image.Pad |
||||||
|
} |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
anchors.fill: parent |
||||||
|
onClicked: { |
||||||
|
switch (button.state) { |
||||||
|
case "": |
||||||
|
TimelineManager.timeline.cacheMedia(model.data.id); |
||||||
|
break; |
||||||
|
case "stopped": |
||||||
|
media.play(); |
||||||
|
console.log("play"); |
||||||
|
button.state = "playing"; |
||||||
|
break; |
||||||
|
case "playing": |
||||||
|
media.pause(); |
||||||
|
console.log("pause"); |
||||||
|
button.state = "stopped"; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
|
||||||
|
MediaPlayer { |
||||||
|
id: media |
||||||
|
|
||||||
|
onError: console.log(errorString) |
||||||
|
onStatusChanged: { |
||||||
|
if (status == MediaPlayer.Loaded) |
||||||
|
progress.updatePositionTexts(); |
||||||
|
|
||||||
|
} |
||||||
|
onStopped: button.state = "stopped" |
||||||
|
} |
||||||
|
|
||||||
|
Connections { |
||||||
|
target: TimelineManager.timeline |
||||||
|
onMediaCached: { |
||||||
|
if (mxcUrl == model.data.url) { |
||||||
|
media.source = "file://" + cacheUrl; |
||||||
|
button.state = "stopped"; |
||||||
|
console.log("media loaded: " + mxcUrl + " at " + cacheUrl); |
||||||
|
} |
||||||
|
console.log("media cached: " + mxcUrl + " at " + cacheUrl); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: col |
||||||
|
|
||||||
|
Text { |
||||||
|
Layout.fillWidth: true |
||||||
|
text: model.data.body |
||||||
|
textFormat: Text.PlainText |
||||||
|
elide: Text.ElideRight |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Text { |
||||||
|
Layout.fillWidth: true |
||||||
|
text: model.data.filesize |
||||||
|
textFormat: Text.PlainText |
||||||
|
elide: Text.ElideRight |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
@ -1,7 +1,12 @@ |
|||||||
import ".." |
import ".." |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
MatrixText { |
MatrixText { |
||||||
property string formatted: model.data.formattedBody |
property string formatted: model.data.formattedBody |
||||||
text: formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") |
|
||||||
width: parent ? parent.width : undefined |
text: "<style type=\"text/css\">a { color:" + colors.link + ";}</style>" + formatted.replace("<pre>", "<pre style='white-space: pre-wrap'>") |
||||||
|
width: parent ? parent.width : undefined |
||||||
|
height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined |
||||||
|
clip: true |
||||||
|
font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize |
||||||
} |
} |
||||||
|
@ -0,0 +1,46 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: qsTr("Awaiting Confirmation") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Label { |
||||||
|
id: content |
||||||
|
|
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: qsTr("Waiting for other side to complete verification.") |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
BusyIndicator { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignLeft |
||||||
|
text: qsTr("Cancel") |
||||||
|
onClicked: { |
||||||
|
flow.cancel(); |
||||||
|
dialog.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,144 @@ |
|||||||
|
import QtQuick 2.10 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Window 2.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
ApplicationWindow { |
||||||
|
id: dialog |
||||||
|
|
||||||
|
property var flow |
||||||
|
|
||||||
|
onClosing: TimelineManager.removeVerificationFlow(flow) |
||||||
|
title: stack.currentItem.title |
||||||
|
flags: Qt.Dialog |
||||||
|
palette: colors |
||||||
|
height: stack.implicitHeight |
||||||
|
width: stack.implicitWidth |
||||||
|
|
||||||
|
StackView { |
||||||
|
id: stack |
||||||
|
|
||||||
|
initialItem: newVerificationRequest |
||||||
|
implicitWidth: currentItem.implicitWidth |
||||||
|
implicitHeight: currentItem.implicitHeight |
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: newVerificationRequest |
||||||
|
|
||||||
|
NewVerificationRequest { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: waiting |
||||||
|
|
||||||
|
Waiting { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: success |
||||||
|
|
||||||
|
Success { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: failed |
||||||
|
|
||||||
|
Failed { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: digitVerification |
||||||
|
|
||||||
|
DigitVerification { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Component { |
||||||
|
id: emojiVerification |
||||||
|
|
||||||
|
EmojiVerification { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
state: flow.state |
||||||
|
states: [ |
||||||
|
State { |
||||||
|
name: "PromptStartVerification" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(newVerificationRequest) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "CompareEmoji" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(emojiVerification) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "CompareNumber" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(digitVerification) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "WaitingForKeys" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(waiting) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "WaitingForOtherToAccept" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(waiting) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "WaitingForMac" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(waiting) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "Success" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(success) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
State { |
||||||
|
name: "Failed" |
||||||
|
|
||||||
|
StateChangeScript { |
||||||
|
script: stack.replace(failed) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: qsTr("Verification Code") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!") |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
|
||||||
|
Label { |
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||||
|
text: flow.sasList[0] |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||||
|
text: flow.sasList[1] |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||||
|
text: flow.sasList[2] |
||||||
|
color: colors.text |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignLeft |
||||||
|
text: qsTr("They do not match!") |
||||||
|
onClicked: { |
||||||
|
flow.cancel(); |
||||||
|
dialog.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
text: qsTr("They match!") |
||||||
|
onClicked: flow.next() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
|
||||||
|
Rectangle { |
||||||
|
color: "red" |
||||||
|
implicitHeight: Qt.application.font.pixelSize * 4 |
||||||
|
implicitWidth: col.width |
||||||
|
height: Qt.application.font.pixelSize * 4 |
||||||
|
width: col.width |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: col |
||||||
|
|
||||||
|
property var emoji: emojis.mapping[Math.floor(Math.random() * 64)] |
||||||
|
|
||||||
|
anchors.bottom: parent.bottom |
||||||
|
|
||||||
|
Label { |
||||||
|
height: font.pixelSize * 2 |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
text: col.emoji.emoji |
||||||
|
font.pixelSize: Qt.application.font.pixelSize * 2 |
||||||
|
} |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
||||||
|
text: col.emoji.description |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: qsTr("Verification failed") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Text { |
||||||
|
id: content |
||||||
|
|
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: { |
||||||
|
switch (flow.error) { |
||||||
|
case DeviceVerificationFlow.UnknownMethod: |
||||||
|
return qsTr("Other client does not support our verification protocol."); |
||||||
|
case DeviceVerificationFlow.MismatchedCommitment: |
||||||
|
case DeviceVerificationFlow.MismatchedSAS: |
||||||
|
case DeviceVerificationFlow.KeyMismatch: |
||||||
|
return qsTr("Key mismatch detected!"); |
||||||
|
case DeviceVerificationFlow.Timeout: |
||||||
|
return qsTr("Device verification timed out."); |
||||||
|
case DeviceVerificationFlow.User: |
||||||
|
return qsTr("Other party canceled the verification."); |
||||||
|
case DeviceVerificationFlow.OutOfOrder: |
||||||
|
return qsTr("Device verification timed out."); |
||||||
|
default: |
||||||
|
return "Unknown verification error."; |
||||||
|
} |
||||||
|
} |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
text: qsTr("Close") |
||||||
|
onClicked: dialog.close() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Label { |
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: flow.sender ? qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.") : qsTr("The device was requested to be verified") |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignLeft |
||||||
|
text: flow.sender ? qsTr("Cancel") : qsTr("Deny") |
||||||
|
onClicked: { |
||||||
|
flow.cancel(); |
||||||
|
dialog.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
text: flow.sender ? qsTr("Start verification") : qsTr("Accept") |
||||||
|
onClicked: flow.next() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: qsTr("Successful Verification") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Label { |
||||||
|
id: content |
||||||
|
|
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: qsTr("Verification successful! Both sides verified their devices!") |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
text: qsTr("Close") |
||||||
|
onClicked: dialog.close() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
import QtQuick 2.3 |
||||||
|
import QtQuick.Controls 2.10 |
||||||
|
import QtQuick.Layouts 1.10 |
||||||
|
import im.nheko 1.0 |
||||||
|
|
||||||
|
Pane { |
||||||
|
property string title: qsTr("Waiting for other party") |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
spacing: 16 |
||||||
|
|
||||||
|
Label { |
||||||
|
id: content |
||||||
|
|
||||||
|
Layout.maximumWidth: 400 |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.fillWidth: true |
||||||
|
wrapMode: Text.Wrap |
||||||
|
text: { |
||||||
|
switch (flow.state) { |
||||||
|
case "WaitingForOtherToAccept": |
||||||
|
return qsTr("Waiting for other side to accept the verification request."); |
||||||
|
case "WaitingForKeys": |
||||||
|
return qsTr("Waiting for other side to continue the verification request."); |
||||||
|
case "WaitingForMac": |
||||||
|
return qsTr("Waiting for other side to complete the verification request."); |
||||||
|
} |
||||||
|
} |
||||||
|
color: colors.text |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
} |
||||||
|
|
||||||
|
BusyIndicator { |
||||||
|
Layout.alignment: Qt.AlignHCenter |
||||||
|
palette: colors |
||||||
|
} |
||||||
|
|
||||||
|
RowLayout { |
||||||
|
Button { |
||||||
|
Layout.alignment: Qt.AlignLeft |
||||||
|
text: qsTr("Cancel") |
||||||
|
onClicked: { |
||||||
|
flow.cancel(); |
||||||
|
dialog.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Item { |
||||||
|
Layout.fillWidth: true |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import "../" |
||||||
|
import QtQuick 2.10 |
||||||
|
import QtQuick.Controls 2.1 |
||||||
|
import im.nheko 1.0 |
||||||
|
import im.nheko.EmojiModel 1.0 |
||||||
|
|
||||||
|
ImageButton { |
||||||
|
id: emojiButton |
||||||
|
|
||||||
|
property var colors: currentActivePalette |
||||||
|
property var emojiPicker |
||||||
|
property string event_id |
||||||
|
|
||||||
|
image: ":/icons/icons/ui/smile.png" |
||||||
|
onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, event_id) |
||||||
|
} |
@ -0,0 +1,332 @@ |
|||||||
|
import "../" |
||||||
|
import QtGraphicalEffects 1.0 |
||||||
|
import QtQuick 2.9 |
||||||
|
import QtQuick.Controls 2.3 |
||||||
|
import QtQuick.Layouts 1.3 |
||||||
|
import im.nheko 1.0 |
||||||
|
import im.nheko.EmojiModel 1.0 |
||||||
|
|
||||||
|
Popup { |
||||||
|
id: emojiPopup |
||||||
|
|
||||||
|
property string event_id |
||||||
|
property var colors |
||||||
|
property alias model: gridView.model |
||||||
|
property var textArea |
||||||
|
property string emojiCategory: "people" |
||||||
|
property real highlightHue: colors.highlight.hslHue |
||||||
|
property real highlightSat: colors.highlight.hslSaturation |
||||||
|
property real highlightLight: colors.highlight.hslLightness |
||||||
|
|
||||||
|
function show(showAt, event_id) { |
||||||
|
console.debug("Showing emojiPicker for " + event_id); |
||||||
|
if (showAt) { |
||||||
|
parent = showAt; |
||||||
|
x = Math.round((showAt.width - width) / 2); |
||||||
|
y = showAt.height; |
||||||
|
} |
||||||
|
emojiPopup.event_id = event_id; |
||||||
|
open(); |
||||||
|
} |
||||||
|
|
||||||
|
margins: 0 |
||||||
|
bottomPadding: 1 |
||||||
|
leftPadding: 1 |
||||||
|
rightPadding: 1 |
||||||
|
modal: true |
||||||
|
focus: true |
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside |
||||||
|
|
||||||
|
ColumnLayout { |
||||||
|
id: columnView |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
spacing: 0 |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
Layout.leftMargin: 3 |
||||||
|
Layout.rightMargin: 3 |
||||||
|
Layout.topMargin: 2 |
||||||
|
|
||||||
|
// emoji grid |
||||||
|
GridView { |
||||||
|
id: gridView |
||||||
|
|
||||||
|
Layout.preferredHeight: emojiPopup.height |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.leftMargin: 4 |
||||||
|
cellWidth: 52 |
||||||
|
cellHeight: 52 |
||||||
|
boundsBehavior: Flickable.StopAtBounds |
||||||
|
clip: true |
||||||
|
currentIndex: -1 // prevent sorting from stealing focus |
||||||
|
|
||||||
|
// Individual emoji |
||||||
|
delegate: AbstractButton { |
||||||
|
width: 48 |
||||||
|
height: 48 |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.text: model.shortName |
||||||
|
ToolTip.visible: hovered |
||||||
|
// TODO: maybe add favorites at some point? |
||||||
|
onClicked: { |
||||||
|
console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id); |
||||||
|
emojiPopup.close(); |
||||||
|
TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode); |
||||||
|
} |
||||||
|
|
||||||
|
// give the emoji a little oomf |
||||||
|
DropShadow { |
||||||
|
width: parent.width |
||||||
|
height: parent.height |
||||||
|
horizontalOffset: 3 |
||||||
|
verticalOffset: 3 |
||||||
|
radius: 8 |
||||||
|
samples: 17 |
||||||
|
color: "#80000000" |
||||||
|
source: parent.contentItem |
||||||
|
} |
||||||
|
|
||||||
|
contentItem: Text { |
||||||
|
horizontalAlignment: Text.AlignHCenter |
||||||
|
verticalAlignment: Text.AlignVCenter |
||||||
|
font.family: Settings.emojiFont |
||||||
|
font.pixelSize: 36 |
||||||
|
text: model.unicode |
||||||
|
} |
||||||
|
|
||||||
|
background: Rectangle { |
||||||
|
anchors.fill: parent |
||||||
|
color: hovered ? colors.highlight : 'transparent' |
||||||
|
radius: 5 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Search field |
||||||
|
header: TextField { |
||||||
|
id: emojiSearch |
||||||
|
|
||||||
|
anchors.left: parent.left |
||||||
|
anchors.right: parent.right |
||||||
|
anchors.rightMargin: emojiScroll.width + 4 |
||||||
|
placeholderText: qsTr("Search") |
||||||
|
selectByMouse: true |
||||||
|
rightPadding: clearSearch.width |
||||||
|
onTextChanged: searchTimer.restart() |
||||||
|
onVisibleChanged: { |
||||||
|
if (visible) |
||||||
|
forceActiveFocus(); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
Timer { |
||||||
|
id: searchTimer |
||||||
|
|
||||||
|
interval: 350 // tweak as needed? |
||||||
|
onTriggered: { |
||||||
|
emojiPopup.model.filter = emojiSearch.text; |
||||||
|
emojiPopup.model.category = EmojiCategory.Search; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
ToolButton { |
||||||
|
id: clearSearch |
||||||
|
|
||||||
|
visible: emojiSearch.text !== '' |
||||||
|
icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? colors.highlight : colors.buttonText) |
||||||
|
focusPolicy: Qt.NoFocus |
||||||
|
onClicked: emojiSearch.clear() |
||||||
|
|
||||||
|
anchors { |
||||||
|
verticalCenter: parent.verticalCenter |
||||||
|
right: parent.right |
||||||
|
} |
||||||
|
// clear the default hover effects. |
||||||
|
|
||||||
|
background: Item { |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { |
||||||
|
id: emojiScroll |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Separator |
||||||
|
Rectangle { |
||||||
|
Layout.fillWidth: true |
||||||
|
Layout.preferredHeight: 1 |
||||||
|
color: emojiPopup.colors.dark |
||||||
|
} |
||||||
|
|
||||||
|
// Category picker row |
||||||
|
RowLayout { |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
Layout.preferredHeight: 42 |
||||||
|
implicitHeight: 42 |
||||||
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
||||||
|
|
||||||
|
// Display the normal categories |
||||||
|
Repeater { |
||||||
|
|
||||||
|
model: ListModel { |
||||||
|
// TODO: Would like to get 'simple' icons for the categories |
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/people.png" |
||||||
|
category: EmojiCategory.People |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/nature.png" |
||||||
|
category: EmojiCategory.Nature |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/foods.png" |
||||||
|
category: EmojiCategory.Food |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/activity.png" |
||||||
|
category: EmojiCategory.Activity |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/travel.png" |
||||||
|
category: EmojiCategory.Travel |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/objects.png" |
||||||
|
category: EmojiCategory.Objects |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/symbols.png" |
||||||
|
category: EmojiCategory.Symbols |
||||||
|
} |
||||||
|
|
||||||
|
ListElement { |
||||||
|
image: ":/icons/icons/emoji-categories/flags.png" |
||||||
|
category: EmojiCategory.Flags |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
delegate: AbstractButton { |
||||||
|
Layout.preferredWidth: 36 |
||||||
|
Layout.preferredHeight: 36 |
||||||
|
hoverEnabled: true |
||||||
|
ToolTip.text: { |
||||||
|
switch (model.category) { |
||||||
|
case EmojiCategory.People: |
||||||
|
return qsTr('People'); |
||||||
|
case EmojiCategory.Nature: |
||||||
|
return qsTr('Nature'); |
||||||
|
case EmojiCategory.Food: |
||||||
|
return qsTr('Food'); |
||||||
|
case EmojiCategory.Activity: |
||||||
|
return qsTr('Activity'); |
||||||
|
case EmojiCategory.Travel: |
||||||
|
return qsTr('Travel'); |
||||||
|
case EmojiCategory.Objects: |
||||||
|
return qsTr('Objects'); |
||||||
|
case EmojiCategory.Symbols: |
||||||
|
return qsTr('Symbols'); |
||||||
|
case EmojiCategory.Flags: |
||||||
|
return qsTr('Flags'); |
||||||
|
} |
||||||
|
} |
||||||
|
ToolTip.visible: hovered |
||||||
|
onClicked: { |
||||||
|
emojiPopup.model.category = model.category; |
||||||
|
} |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
id: mouseArea |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
onPressed: mouse.accepted = false |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
|
||||||
|
contentItem: Image { |
||||||
|
horizontalAlignment: Image.AlignHCenter |
||||||
|
verticalAlignment: Image.AlignVCenter |
||||||
|
fillMode: Image.Pad |
||||||
|
sourceSize.width: 32 |
||||||
|
sourceSize.height: 32 |
||||||
|
source: "image://colorimage/" + model.image + "?" + (hovered ? colors.highlight : colors.buttonText) |
||||||
|
} |
||||||
|
|
||||||
|
background: Rectangle { |
||||||
|
anchors.fill: parent |
||||||
|
color: emojiPopup.model.category === model.category ? Qt.hsla(highlightHue, highlightSat, highlightLight, 0.2) : 'transparent' |
||||||
|
radius: 5 |
||||||
|
border.color: emojiPopup.model.category === model.category ? colors.highlight : 'transparent' |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Separator |
||||||
|
Rectangle { |
||||||
|
Layout.fillHeight: true |
||||||
|
Layout.preferredWidth: 1 |
||||||
|
implicitWidth: 1 |
||||||
|
height: parent.height |
||||||
|
color: emojiPopup.colors.dark |
||||||
|
} |
||||||
|
|
||||||
|
// Search Button is special |
||||||
|
AbstractButton { |
||||||
|
id: searchBtn |
||||||
|
|
||||||
|
hoverEnabled: true |
||||||
|
Layout.alignment: Qt.AlignRight |
||||||
|
Layout.bottomMargin: 0 |
||||||
|
ToolTip.text: qsTr("Search") |
||||||
|
ToolTip.visible: hovered |
||||||
|
onClicked: { |
||||||
|
// clear any filters |
||||||
|
emojiPopup.model.category = EmojiCategory.Search; |
||||||
|
gridView.positionViewAtBeginning(); |
||||||
|
emojiSearch.forceActiveFocus(); |
||||||
|
} |
||||||
|
Layout.preferredWidth: 36 |
||||||
|
Layout.preferredHeight: 36 |
||||||
|
implicitWidth: 36 |
||||||
|
implicitHeight: 36 |
||||||
|
|
||||||
|
MouseArea { |
||||||
|
id: mouseArea |
||||||
|
|
||||||
|
anchors.fill: parent |
||||||
|
onPressed: mouse.accepted = false |
||||||
|
cursorShape: Qt.PointingHandCursor |
||||||
|
} |
||||||
|
|
||||||
|
contentItem: Image { |
||||||
|
anchors.right: parent.right |
||||||
|
horizontalAlignment: Image.AlignHCenter |
||||||
|
verticalAlignment: Image.AlignVCenter |
||||||
|
sourceSize.width: 32 |
||||||
|
sourceSize.height: 32 |
||||||
|
fillMode: Image.Pad |
||||||
|
smooth: true |
||||||
|
source: "image://colorimage/:/icons/icons/ui/search.png?" + (parent.hovered ? colors.highlight : colors.buttonText) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
[Controls] |
||||||
|
FallbackStyle=Fusion |
@ -0,0 +1,38 @@ |
|||||||
|
#include "BlurhashProvider.h" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
|
||||||
|
#include <QUrl> |
||||||
|
|
||||||
|
#include "blurhash.hpp" |
||||||
|
|
||||||
|
void |
||||||
|
BlurhashResponse::run() |
||||||
|
{ |
||||||
|
if (m_requestedSize.width() < 0 || m_requestedSize.height() < 0) { |
||||||
|
m_error = QStringLiteral("Blurhash needs size request"); |
||||||
|
emit finished(); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (m_requestedSize.width() == 0 || m_requestedSize.height() == 0) { |
||||||
|
m_image = QImage(m_requestedSize, QImage::Format_RGB32); |
||||||
|
m_image.fill(QColor(0, 0, 0)); |
||||||
|
emit finished(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
auto decoded = blurhash::decode(QUrl::fromPercentEncoding(m_id.toUtf8()).toStdString(), |
||||||
|
m_requestedSize.width(), |
||||||
|
m_requestedSize.height(), |
||||||
|
4); |
||||||
|
if (decoded.image.empty()) { |
||||||
|
m_error = QStringLiteral("Failed decode!"); |
||||||
|
emit finished(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
QImage image(decoded.image.data(), decoded.width, decoded.height, QImage::Format_RGB32); |
||||||
|
|
||||||
|
m_image = image.copy(); |
||||||
|
emit finished(); |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QQuickAsyncImageProvider> |
||||||
|
#include <QQuickImageResponse> |
||||||
|
|
||||||
|
#include <QImage> |
||||||
|
#include <QThreadPool> |
||||||
|
|
||||||
|
class BlurhashResponse |
||||||
|
: public QQuickImageResponse |
||||||
|
, public QRunnable |
||||||
|
{ |
||||||
|
public: |
||||||
|
BlurhashResponse(const QString &id, const QSize &requestedSize) |
||||||
|
|
||||||
|
: m_id(id) |
||||||
|
, m_requestedSize(requestedSize) |
||||||
|
{ |
||||||
|
setAutoDelete(false); |
||||||
|
} |
||||||
|
|
||||||
|
QQuickTextureFactory *textureFactory() const override |
||||||
|
{ |
||||||
|
return QQuickTextureFactory::textureFactoryForImage(m_image); |
||||||
|
} |
||||||
|
QString errorString() const override { return m_error; } |
||||||
|
|
||||||
|
void run() override; |
||||||
|
|
||||||
|
QString m_id, m_error; |
||||||
|
QSize m_requestedSize; |
||||||
|
QImage m_image; |
||||||
|
}; |
||||||
|
|
||||||
|
class BlurhashProvider |
||||||
|
: public QObject |
||||||
|
, public QQuickAsyncImageProvider |
||||||
|
{ |
||||||
|
Q_OBJECT |
||||||
|
public slots: |
||||||
|
QQuickImageResponse *requestImageResponse(const QString &id, |
||||||
|
const QSize &requestedSize) override |
||||||
|
{ |
||||||
|
BlurhashResponse *response = new BlurhashResponse(id, requestedSize); |
||||||
|
pool.start(response); |
||||||
|
return response; |
||||||
|
} |
||||||
|
|
||||||
|
private: |
||||||
|
QThreadPool pool; |
||||||
|
}; |
@ -0,0 +1,457 @@ |
|||||||
|
#include <algorithm> |
||||||
|
#include <cctype> |
||||||
|
#include <chrono> |
||||||
|
#include <cstdint> |
||||||
|
|
||||||
|
#include <QMediaPlaylist> |
||||||
|
#include <QUrl> |
||||||
|
|
||||||
|
#include "Cache.h" |
||||||
|
#include "CallManager.h" |
||||||
|
#include "ChatPage.h" |
||||||
|
#include "Logging.h" |
||||||
|
#include "MainWindow.h" |
||||||
|
#include "MatrixClient.h" |
||||||
|
#include "UserSettingsPage.h" |
||||||
|
#include "WebRTCSession.h" |
||||||
|
#include "dialogs/AcceptCall.h" |
||||||
|
|
||||||
|
#include "mtx/responses/turn_server.hpp" |
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>) |
||||||
|
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate) |
||||||
|
Q_DECLARE_METATYPE(mtx::responses::TurnServer) |
||||||
|
|
||||||
|
using namespace mtx::events; |
||||||
|
using namespace mtx::events::msg; |
||||||
|
|
||||||
|
// https://github.com/vector-im/riot-web/issues/10173
|
||||||
|
#define STUN_SERVER "stun://turn.matrix.org:3478"
|
||||||
|
|
||||||
|
namespace { |
||||||
|
std::vector<std::string> |
||||||
|
getTurnURIs(const mtx::responses::TurnServer &turnServer); |
||||||
|
} |
||||||
|
|
||||||
|
CallManager::CallManager(QSharedPointer<UserSettings> userSettings) |
||||||
|
: QObject() |
||||||
|
, session_(WebRTCSession::instance()) |
||||||
|
, turnServerTimer_(this) |
||||||
|
, settings_(userSettings) |
||||||
|
{ |
||||||
|
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>(); |
||||||
|
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>(); |
||||||
|
qRegisterMetaType<mtx::responses::TurnServer>(); |
||||||
|
|
||||||
|
connect( |
||||||
|
&session_, |
||||||
|
&WebRTCSession::offerCreated, |
||||||
|
this, |
||||||
|
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_); |
||||||
|
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_}); |
||||||
|
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); |
||||||
|
QTimer::singleShot(timeoutms_, this, [this]() { |
||||||
|
if (session_.state() == webrtc::State::OFFERSENT) { |
||||||
|
hangUp(CallHangUp::Reason::InviteTimeOut); |
||||||
|
emit ChatPage::instance()->showNotification( |
||||||
|
"The remote side failed to pick up."); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
connect( |
||||||
|
&session_, |
||||||
|
&WebRTCSession::answerCreated, |
||||||
|
this, |
||||||
|
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) { |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_); |
||||||
|
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0}); |
||||||
|
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0}); |
||||||
|
}); |
||||||
|
|
||||||
|
connect(&session_, |
||||||
|
&WebRTCSession::newICECandidate, |
||||||
|
this, |
||||||
|
[this](const CallCandidates::Candidate &candidate) { |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_); |
||||||
|
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0}); |
||||||
|
}); |
||||||
|
|
||||||
|
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer); |
||||||
|
|
||||||
|
connect(this, |
||||||
|
&CallManager::turnServerRetrieved, |
||||||
|
this, |
||||||
|
[this](const mtx::responses::TurnServer &res) { |
||||||
|
nhlog::net()->info("TURN server(s) retrieved from homeserver:"); |
||||||
|
nhlog::net()->info("username: {}", res.username); |
||||||
|
nhlog::net()->info("ttl: {} seconds", res.ttl); |
||||||
|
for (const auto &u : res.uris) |
||||||
|
nhlog::net()->info("uri: {}", u); |
||||||
|
|
||||||
|
// Request new credentials close to expiry
|
||||||
|
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||||
|
turnURIs_ = getTurnURIs(res); |
||||||
|
uint32_t ttl = std::max(res.ttl, UINT32_C(3600)); |
||||||
|
if (res.ttl < 3600) |
||||||
|
nhlog::net()->warn("Setting ttl to 1 hour"); |
||||||
|
turnServerTimer_.setInterval(ttl * 1000 * 0.9); |
||||||
|
}); |
||||||
|
|
||||||
|
connect(&session_, &WebRTCSession::stateChanged, this, [this](webrtc::State state) { |
||||||
|
switch (state) { |
||||||
|
case webrtc::State::DISCONNECTED: |
||||||
|
playRingtone("qrc:/media/media/callend.ogg", false); |
||||||
|
clear(); |
||||||
|
break; |
||||||
|
case webrtc::State::ICEFAILED: { |
||||||
|
QString error("Call connection failed."); |
||||||
|
if (turnURIs_.empty()) |
||||||
|
error += " Your homeserver has no configured TURN server."; |
||||||
|
emit ChatPage::instance()->showNotification(error); |
||||||
|
hangUp(CallHangUp::Reason::ICEFailed); |
||||||
|
break; |
||||||
|
} |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
connect(&player_, |
||||||
|
&QMediaPlayer::mediaStatusChanged, |
||||||
|
this, |
||||||
|
[this](QMediaPlayer::MediaStatus status) { |
||||||
|
if (status == QMediaPlayer::LoadedMedia) |
||||||
|
player_.play(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::sendInvite(const QString &roomid) |
||||||
|
{ |
||||||
|
if (onActiveCall()) |
||||||
|
return; |
||||||
|
|
||||||
|
auto roomInfo = cache::singleRoomInfo(roomid.toStdString()); |
||||||
|
if (roomInfo.member_count != 2) { |
||||||
|
emit ChatPage::instance()->showNotification( |
||||||
|
"Voice calls are limited to 1:1 rooms."); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
std::string errorMessage; |
||||||
|
if (!session_.init(&errorMessage)) { |
||||||
|
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
roomid_ = roomid; |
||||||
|
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); |
||||||
|
session_.setTurnServers(turnURIs_); |
||||||
|
|
||||||
|
generateCallID(); |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_); |
||||||
|
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString())); |
||||||
|
const RoomMember &callee = |
||||||
|
members.front().user_id == utils::localUser() ? members.back() : members.front(); |
||||||
|
callPartyName_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name; |
||||||
|
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); |
||||||
|
emit newCallParty(); |
||||||
|
playRingtone("qrc:/media/media/ringback.ogg", true); |
||||||
|
if (!session_.createOffer()) { |
||||||
|
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||||
|
endCall(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
std::string |
||||||
|
callHangUpReasonString(CallHangUp::Reason reason) |
||||||
|
{ |
||||||
|
switch (reason) { |
||||||
|
case CallHangUp::Reason::ICEFailed: |
||||||
|
return "ICE failed"; |
||||||
|
case CallHangUp::Reason::InviteTimeOut: |
||||||
|
return "Invite time out"; |
||||||
|
default: |
||||||
|
return "User"; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::hangUp(CallHangUp::Reason reason) |
||||||
|
{ |
||||||
|
if (!callid_.empty()) { |
||||||
|
nhlog::ui()->debug( |
||||||
|
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason)); |
||||||
|
emit newMessage(roomid_, CallHangUp{callid_, 0, reason}); |
||||||
|
endCall(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool |
||||||
|
CallManager::onActiveCall() const |
||||||
|
{ |
||||||
|
return session_.state() != webrtc::State::DISCONNECTED; |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event) |
||||||
|
{ |
||||||
|
#ifdef GSTREAMER_AVAILABLE |
||||||
|
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) || |
||||||
|
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event)) |
||||||
|
return; |
||||||
|
#else |
||||||
|
(void)event; |
||||||
|
#endif |
||||||
|
} |
||||||
|
|
||||||
|
template<typename T> |
||||||
|
bool |
||||||
|
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event) |
||||||
|
{ |
||||||
|
if (std::holds_alternative<RoomEvent<T>>(event)) { |
||||||
|
handleEvent(std::get<RoomEvent<T>>(event)); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent) |
||||||
|
{ |
||||||
|
const char video[] = "m=video"; |
||||||
|
const std::string &sdp = callInviteEvent.content.sdp; |
||||||
|
bool isVideo = std::search(sdp.cbegin(), |
||||||
|
sdp.cend(), |
||||||
|
std::cbegin(video), |
||||||
|
std::cend(video) - 1, |
||||||
|
[](unsigned char c1, unsigned char c2) { |
||||||
|
return std::tolower(c1) == std::tolower(c2); |
||||||
|
}) != sdp.cend(); |
||||||
|
|
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming {} CallInvite from {}", |
||||||
|
callInviteEvent.content.call_id, |
||||||
|
(isVideo ? "video" : "voice"), |
||||||
|
callInviteEvent.sender); |
||||||
|
|
||||||
|
if (callInviteEvent.content.call_id.empty()) |
||||||
|
return; |
||||||
|
|
||||||
|
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id); |
||||||
|
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) { |
||||||
|
emit newMessage(QString::fromStdString(callInviteEvent.room_id), |
||||||
|
CallHangUp{callInviteEvent.content.call_id, |
||||||
|
0, |
||||||
|
CallHangUp::Reason::InviteTimeOut}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
playRingtone("qrc:/media/media/ring.ogg", true); |
||||||
|
roomid_ = QString::fromStdString(callInviteEvent.room_id); |
||||||
|
callid_ = callInviteEvent.content.call_id; |
||||||
|
remoteICECandidates_.clear(); |
||||||
|
|
||||||
|
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id)); |
||||||
|
const RoomMember &caller = |
||||||
|
members.front().user_id == utils::localUser() ? members.back() : members.front(); |
||||||
|
callPartyName_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name; |
||||||
|
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); |
||||||
|
emit newCallParty(); |
||||||
|
auto dialog = new dialogs::AcceptCall(caller.user_id, |
||||||
|
caller.display_name, |
||||||
|
QString::fromStdString(roomInfo.name), |
||||||
|
QString::fromStdString(roomInfo.avatar_url), |
||||||
|
settings_, |
||||||
|
MainWindow::instance()); |
||||||
|
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() { |
||||||
|
MainWindow::instance()->hideOverlay(); |
||||||
|
answerInvite(callInviteEvent.content); |
||||||
|
}); |
||||||
|
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() { |
||||||
|
MainWindow::instance()->hideOverlay(); |
||||||
|
hangUp(); |
||||||
|
}); |
||||||
|
MainWindow::instance()->showSolidOverlayModal(dialog); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::answerInvite(const CallInvite &invite) |
||||||
|
{ |
||||||
|
stopRingtone(); |
||||||
|
std::string errorMessage; |
||||||
|
if (!session_.init(&errorMessage)) { |
||||||
|
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage)); |
||||||
|
hangUp(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); |
||||||
|
session_.setTurnServers(turnURIs_); |
||||||
|
|
||||||
|
if (!session_.acceptOffer(invite.sdp)) { |
||||||
|
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||||
|
hangUp(); |
||||||
|
return; |
||||||
|
} |
||||||
|
session_.acceptICECandidates(remoteICECandidates_); |
||||||
|
remoteICECandidates_.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent) |
||||||
|
{ |
||||||
|
if (callCandidatesEvent.sender == utils::localUser().toStdString()) |
||||||
|
return; |
||||||
|
|
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}", |
||||||
|
callCandidatesEvent.content.call_id, |
||||||
|
callCandidatesEvent.sender); |
||||||
|
|
||||||
|
if (callid_ == callCandidatesEvent.content.call_id) { |
||||||
|
if (onActiveCall()) |
||||||
|
session_.acceptICECandidates(callCandidatesEvent.content.candidates); |
||||||
|
else { |
||||||
|
// CallInvite has been received and we're awaiting localUser to accept or
|
||||||
|
// reject the call
|
||||||
|
for (const auto &c : callCandidatesEvent.content.candidates) |
||||||
|
remoteICECandidates_.push_back(c); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent) |
||||||
|
{ |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}", |
||||||
|
callAnswerEvent.content.call_id, |
||||||
|
callAnswerEvent.sender); |
||||||
|
|
||||||
|
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() && |
||||||
|
callid_ == callAnswerEvent.content.call_id) { |
||||||
|
emit ChatPage::instance()->showNotification("Call answered on another device."); |
||||||
|
stopRingtone(); |
||||||
|
MainWindow::instance()->hideOverlay(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) { |
||||||
|
stopRingtone(); |
||||||
|
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) { |
||||||
|
emit ChatPage::instance()->showNotification("Problem setting up call."); |
||||||
|
hangUp(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent) |
||||||
|
{ |
||||||
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}", |
||||||
|
callHangUpEvent.content.call_id, |
||||||
|
callHangUpReasonString(callHangUpEvent.content.reason), |
||||||
|
callHangUpEvent.sender); |
||||||
|
|
||||||
|
if (callid_ == callHangUpEvent.content.call_id) { |
||||||
|
MainWindow::instance()->hideOverlay(); |
||||||
|
endCall(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::generateCallID() |
||||||
|
{ |
||||||
|
using namespace std::chrono; |
||||||
|
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); |
||||||
|
callid_ = "c" + std::to_string(ms); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::clear() |
||||||
|
{ |
||||||
|
roomid_.clear(); |
||||||
|
callPartyName_.clear(); |
||||||
|
callPartyAvatarUrl_.clear(); |
||||||
|
callid_.clear(); |
||||||
|
remoteICECandidates_.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::endCall() |
||||||
|
{ |
||||||
|
stopRingtone(); |
||||||
|
clear(); |
||||||
|
session_.end(); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::refreshTurnServer() |
||||||
|
{ |
||||||
|
turnURIs_.clear(); |
||||||
|
turnServerTimer_.start(2000); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::retrieveTurnServer() |
||||||
|
{ |
||||||
|
http::client()->get_turn_server( |
||||||
|
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) { |
||||||
|
if (err) { |
||||||
|
turnServerTimer_.setInterval(5000); |
||||||
|
return; |
||||||
|
} |
||||||
|
emit turnServerRetrieved(res); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::playRingtone(const QString &ringtone, bool repeat) |
||||||
|
{ |
||||||
|
static QMediaPlaylist playlist; |
||||||
|
playlist.clear(); |
||||||
|
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop |
||||||
|
: QMediaPlaylist::CurrentItemOnce); |
||||||
|
playlist.addMedia(QUrl(ringtone)); |
||||||
|
player_.setVolume(100); |
||||||
|
player_.setPlaylist(&playlist); |
||||||
|
} |
||||||
|
|
||||||
|
void |
||||||
|
CallManager::stopRingtone() |
||||||
|
{ |
||||||
|
player_.setPlaylist(nullptr); |
||||||
|
} |
||||||
|
|
||||||
|
namespace { |
||||||
|
std::vector<std::string> |
||||||
|
getTurnURIs(const mtx::responses::TurnServer &turnServer) |
||||||
|
{ |
||||||
|
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
|
||||||
|
// where username and password are percent-encoded
|
||||||
|
std::vector<std::string> ret; |
||||||
|
for (const auto &uri : turnServer.uris) { |
||||||
|
if (auto c = uri.find(':'); c == std::string::npos) { |
||||||
|
nhlog::ui()->error("Invalid TURN server uri: {}", uri); |
||||||
|
continue; |
||||||
|
} else { |
||||||
|
std::string scheme = std::string(uri, 0, c); |
||||||
|
if (scheme != "turn" && scheme != "turns") { |
||||||
|
nhlog::ui()->error("Invalid TURN server uri: {}", uri); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
QString encodedUri = |
||||||
|
QString::fromStdString(scheme) + "://" + |
||||||
|
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + |
||||||
|
":" + |
||||||
|
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + |
||||||
|
"@" + QString::fromStdString(std::string(uri, ++c)); |
||||||
|
ret.push_back(encodedUri.toStdString()); |
||||||
|
} |
||||||
|
} |
||||||
|
return ret; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include <QMediaPlayer> |
||||||
|
#include <QObject> |
||||||
|
#include <QSharedPointer> |
||||||
|
#include <QString> |
||||||
|
#include <QTimer> |
||||||
|
|
||||||
|
#include "mtx/events/collections.hpp" |
||||||
|
#include "mtx/events/voip.hpp" |
||||||
|
|
||||||
|
namespace mtx::responses { |
||||||
|
struct TurnServer; |
||||||
|
} |
||||||
|
|
||||||
|
class UserSettings; |
||||||
|
class WebRTCSession; |
||||||
|
|
||||||
|
class CallManager : public QObject |
||||||
|
{ |
||||||
|
Q_OBJECT |
||||||
|
|
||||||
|
public: |
||||||
|
CallManager(QSharedPointer<UserSettings>); |
||||||
|
|
||||||
|
void sendInvite(const QString &roomid); |
||||||
|
void hangUp( |
||||||
|
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User); |
||||||
|
bool onActiveCall() const; |
||||||
|
QString callPartyName() const { return callPartyName_; } |
||||||
|
QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } |
||||||
|
void refreshTurnServer(); |
||||||
|
|
||||||
|
public slots: |
||||||
|
void syncEvent(const mtx::events::collections::TimelineEvents &event); |
||||||
|
|
||||||
|
signals: |
||||||
|
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &); |
||||||
|
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &); |
||||||
|
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &); |
||||||
|
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &); |
||||||
|
void newCallParty(); |
||||||
|
void turnServerRetrieved(const mtx::responses::TurnServer &); |
||||||
|
|
||||||
|
private slots: |
||||||
|
void retrieveTurnServer(); |
||||||
|
|
||||||
|
private: |
||||||
|
WebRTCSession &session_; |
||||||
|
QString roomid_; |
||||||
|
QString callPartyName_; |
||||||
|
QString callPartyAvatarUrl_; |
||||||
|
std::string callid_; |
||||||
|
const uint32_t timeoutms_ = 120000; |
||||||
|
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_; |
||||||
|
std::vector<std::string> turnURIs_; |
||||||
|
QTimer turnServerTimer_; |
||||||
|
QSharedPointer<UserSettings> settings_; |
||||||
|
QMediaPlayer player_; |
||||||
|
|
||||||
|
template<typename T> |
||||||
|
bool handleEvent_(const mtx::events::collections::TimelineEvents &event); |
||||||
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &); |
||||||
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &); |
||||||
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &); |
||||||
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &); |
||||||
|
void answerInvite(const mtx::events::msg::CallInvite &); |
||||||
|
void generateCallID(); |
||||||
|
void clear(); |
||||||
|
void endCall(); |
||||||
|
void playRingtone(const QString &ringtone, bool repeat); |
||||||
|
void stopRingtone(); |
||||||
|
}; |